feat: add qtfm decipher
This commit is contained in:
parent
6f229ff39e
commit
ddc9628305
@ -40,7 +40,7 @@ importers:
|
|||||||
version: 2.0.1(react-redux@9.0.4(@types/react@18.2.45)(react@18.2.0)(redux@5.0.0))(react@18.2.0)
|
version: 2.0.1(react-redux@9.0.4(@types/react@18.2.45)(react@18.2.0)(redux@5.0.0))(react@18.2.0)
|
||||||
'@unlock-music/crypto':
|
'@unlock-music/crypto':
|
||||||
specifier: 0.0.0-alpha.15
|
specifier: 0.0.0-alpha.15
|
||||||
version: 0.0.0-alpha.15
|
version: link:../lib_um_crypto_rust/um_wasm_loader
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^10.16.16
|
specifier: ^10.16.16
|
||||||
version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@ -1921,9 +1921,6 @@ packages:
|
|||||||
'@ungap/structured-clone@1.2.0':
|
'@ungap/structured-clone@1.2.0':
|
||||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||||
|
|
||||||
'@unlock-music/crypto@0.0.0-alpha.15':
|
|
||||||
resolution: {integrity: sha512-ST3Vbv5ITWE2n8W+07DiPUSpNy2qsK6nELsKcAeAXlulL/HirTfky5xpJl3Q6Zy/34crMsG8jKvP5QB6ELbsSw==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.15/crypto-0.0.0-alpha.15.tgz}
|
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.2.1':
|
'@vitejs/plugin-react@4.2.1':
|
||||||
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
|
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
@ -6110,8 +6107,6 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.2.0': {}
|
'@ungap/structured-clone@1.2.0': {}
|
||||||
|
|
||||||
'@unlock-music/crypto@0.0.0-alpha.15': {}
|
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))':
|
'@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.6
|
'@babel/core': 7.23.6
|
||||||
|
@ -6,6 +6,7 @@ import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts';
|
|||||||
import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts';
|
import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts';
|
||||||
import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts';
|
import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts';
|
||||||
import { XiamiDecipher } from '~/decrypt-worker/decipher/XiamiMusic.ts';
|
import { XiamiDecipher } from '~/decrypt-worker/decipher/XiamiMusic.ts';
|
||||||
|
import { QignTingFMDecipher } from '~/decrypt-worker/decipher/QingTingFM.ts';
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
OK = 0,
|
OK = 0,
|
||||||
@ -54,6 +55,9 @@ export const allCryptoFactories: DecipherFactory[] = [
|
|||||||
// Xiami (*.xm)
|
// Xiami (*.xm)
|
||||||
XiamiDecipher.make,
|
XiamiDecipher.make,
|
||||||
|
|
||||||
|
// QingTingFM Android (*.qta)
|
||||||
|
QignTingFMDecipher.make,
|
||||||
|
|
||||||
/// File with a fixed footer goes second
|
/// File with a fixed footer goes second
|
||||||
|
|
||||||
// QMCv2 (*.mflac)
|
// QMCv2 (*.mflac)
|
||||||
@ -75,9 +79,6 @@ export const allCryptoFactories: DecipherFactory[] = [
|
|||||||
XimalayaAndroidDecipher.makeX2M,
|
XimalayaAndroidDecipher.makeX2M,
|
||||||
XimalayaAndroidDecipher.makeX3M,
|
XimalayaAndroidDecipher.makeX3M,
|
||||||
|
|
||||||
// QingTingFM (Android)
|
|
||||||
// QingTingFM$Device.make,
|
|
||||||
|
|
||||||
// Transparent crypto (not encrypted)
|
// Transparent crypto (not encrypted)
|
||||||
TransparentDecipher.make,
|
TransparentDecipher.make,
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export enum DECRYPTION_WORKER_ACTION_NAME {
|
export enum DECRYPTION_WORKER_ACTION_NAME {
|
||||||
DECRYPT = 'DECRYPT',
|
DECRYPT = 'DECRYPT',
|
||||||
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
|
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
|
||||||
|
QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY',
|
||||||
VERSION = 'VERSION',
|
VERSION = 'VERSION',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
src/decrypt-worker/decipher/QingTingFM.ts
Normal file
37
src/decrypt-worker/decipher/QingTingFM.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||||
|
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts';
|
||||||
|
import { QingTingFM } from '@unlock-music/crypto';
|
||||||
|
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||||
|
import { unhex } from '~/util/hex.ts';
|
||||||
|
|
||||||
|
export class QignTingFMDecipher implements DecipherInstance {
|
||||||
|
cipherName = 'QingTingFM (Android, qta)';
|
||||||
|
|
||||||
|
async decrypt(buffer: Uint8Array, opts: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||||
|
const key = unhex(opts.qingTingAndroidKey || '');
|
||||||
|
const iv = QingTingFM.getFileIV(opts.fileName);
|
||||||
|
|
||||||
|
if (key.byteLength !== 16 || iv.byteLength !== 16) {
|
||||||
|
return {
|
||||||
|
status: Status.FAILED,
|
||||||
|
message: 'device key or iv invalid',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const qtfm = new QingTingFM(key, iv);
|
||||||
|
const audioBuffer = new Uint8Array(buffer);
|
||||||
|
for (const [block, i] of chunkBuffer(audioBuffer)) {
|
||||||
|
qtfm.decrypt(block, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cipherName: this.cipherName,
|
||||||
|
status: Status.OK,
|
||||||
|
data: audioBuffer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static make() {
|
||||||
|
return new QignTingFMDecipher();
|
||||||
|
}
|
||||||
|
}
|
@ -15,3 +15,12 @@ export interface FetchMusicExNamePayload {
|
|||||||
id: string;
|
id: string;
|
||||||
blobURI: string;
|
blobURI: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetQingTingFMDeviceKeyPayload {
|
||||||
|
product: string;
|
||||||
|
device: string;
|
||||||
|
manufacturer: string;
|
||||||
|
brand: string;
|
||||||
|
board: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
@ -2,8 +2,9 @@ import { WorkerServerBus } from '~/util/WorkerEventBus';
|
|||||||
import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
|
import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
|
||||||
import { getUmcVersion } from '@unlock-music/crypto';
|
import { getUmcVersion } from '@unlock-music/crypto';
|
||||||
|
|
||||||
import { workerDecryptHandler } from './worker/handler/decrypt';
|
import { workerDecryptHandler } from './worker/decrypt.ts';
|
||||||
import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser';
|
import { workerParseMusicExMediaName } from './worker/qmcv2_parser.ts';
|
||||||
|
import { workerGetQtfmDeviceKey } from '~/decrypt-worker/worker/qtfm_device_key.ts';
|
||||||
|
|
||||||
const bus = new WorkerServerBus();
|
const bus = new WorkerServerBus();
|
||||||
onmessage = bus.onmessage;
|
onmessage = bus.onmessage;
|
||||||
@ -11,3 +12,4 @@ onmessage = bus.onmessage;
|
|||||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
|
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
|
||||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName);
|
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName);
|
||||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getUmcVersion);
|
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getUmcVersion);
|
||||||
|
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils';
|
import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils.ts';
|
||||||
import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types';
|
import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types.ts';
|
||||||
import { allCryptoFactories } from '../../Deciphers.ts';
|
import { allCryptoFactories } from '../Deciphers.ts';
|
||||||
import { toBlob } from '~/decrypt-worker/util/buffer';
|
import { toBlob } from '~/decrypt-worker/util/buffer.ts';
|
||||||
import { DecipherFactory, DecipherInstance, Status } from '~/decrypt-worker/Deciphers.ts';
|
import { DecipherFactory, DecipherInstance, Status } from '~/decrypt-worker/Deciphers.ts';
|
||||||
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError';
|
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
|
||||||
import { ready as umCryptoReady } from '@unlock-music/crypto';
|
import { ready as umCryptoReady } from '../../../../lib_um_crypto_rust/um_wasm_loader';
|
||||||
import { go } from '~/util/go.ts';
|
import { go } from '~/util/go.ts';
|
||||||
import { detectAudioExtension } from '~/decrypt-worker/util/audioType.ts';
|
import { detectAudioExtension } from '~/decrypt-worker/util/audioType.ts';
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types.ts';
|
||||||
import { timedLogger } from '~/util/logUtils.ts';
|
import { timedLogger } from '~/util/logUtils.ts';
|
||||||
import { QMCFooter } from '@unlock-music/crypto';
|
import { QMCFooter } from '@unlock-music/crypto';
|
||||||
|
|
15
src/decrypt-worker/worker/qtfm_device_key.ts
Normal file
15
src/decrypt-worker/worker/qtfm_device_key.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts';
|
||||||
|
import { QingTingFM } from '@unlock-music/crypto';
|
||||||
|
import { hex } from '~/util/hex.ts';
|
||||||
|
|
||||||
|
export async function workerGetQtfmDeviceKey({
|
||||||
|
device,
|
||||||
|
brand,
|
||||||
|
model,
|
||||||
|
product,
|
||||||
|
manufacturer,
|
||||||
|
board,
|
||||||
|
}: GetQingTingFMDeviceKeyPayload) {
|
||||||
|
const buffer = QingTingFM.getDeviceKey(device, brand, model, product, manufacturer, board);
|
||||||
|
return hex(buffer);
|
||||||
|
}
|
@ -18,6 +18,9 @@ import { ChangeEvent, ClipboardEvent } from 'react';
|
|||||||
import { VQuote } from '~/components/HelpText/VQuote';
|
import { VQuote } from '~/components/HelpText/VQuote';
|
||||||
import { selectStagingQtfmAndroidKey } from '../settingsSelector';
|
import { selectStagingQtfmAndroidKey } from '../settingsSelector';
|
||||||
import { qtfmAndroidUpdateKey } from '../settingsSlice';
|
import { qtfmAndroidUpdateKey } from '../settingsSlice';
|
||||||
|
import { workerClientBus } from '~/decrypt-worker/client.ts';
|
||||||
|
import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts';
|
||||||
|
import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts';
|
||||||
|
|
||||||
const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
|
const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
|
||||||
|
|
||||||
@ -37,29 +40,23 @@ export function PanelQingTing() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataMap = new Map();
|
const dataMap = Object.create(null);
|
||||||
for (const [_unused, key, value] of plainText.matchAll(
|
for (const [, key, value] of plainText.matchAll(/^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim)) {
|
||||||
/^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim,
|
dataMap[key.toLowerCase()] = value;
|
||||||
)) {
|
|
||||||
dataMap.set(key.toLowerCase(), value);
|
|
||||||
}
|
}
|
||||||
|
const { product, device, manufacturer, brand, board, model } = dataMap;
|
||||||
|
|
||||||
const product = dataMap.get('product') ?? null;
|
if (product && device && manufacturer && brand && board && model) {
|
||||||
const device = dataMap.get('device') ?? null;
|
|
||||||
const manufacturer = dataMap.get('manufacturer') ?? null;
|
|
||||||
const brand = dataMap.get('brand') ?? null;
|
|
||||||
const board = dataMap.get('board') ?? null;
|
|
||||||
const model = dataMap.get('model') ?? null;
|
|
||||||
if (
|
|
||||||
product !== null &&
|
|
||||||
device !== null &&
|
|
||||||
manufacturer !== null &&
|
|
||||||
brand !== null &&
|
|
||||||
board !== null &&
|
|
||||||
model !== null
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('TODO!');
|
workerClientBus
|
||||||
|
.request<string, GetQingTingFMDeviceKeyPayload>(
|
||||||
|
DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY,
|
||||||
|
dataMap,
|
||||||
|
)
|
||||||
|
.then(setSecretKey)
|
||||||
|
.catch((err) => {
|
||||||
|
alert(`生成设备密钥时发生错误: ${err}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
||||||
import { formatHex } from './formatHex';
|
import { formatHex } from './hex.ts';
|
||||||
|
|
||||||
export class MMKVParser {
|
export class MMKVParser {
|
||||||
private offset = 4;
|
private offset = 4;
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export function formatHex(value: number, len = 8) {
|
|
||||||
return '0x' + (value | 0).toString(16).padStart(len, '0');
|
|
||||||
}
|
|
15
src/util/hex.ts
Normal file
15
src/util/hex.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export function formatHex(value: number, len = 8) {
|
||||||
|
return '0x' + (value | 0).toString(16).padStart(len, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hex(value: Uint8Array): string {
|
||||||
|
return Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unhex(value: string): Uint8Array {
|
||||||
|
const bytes = [];
|
||||||
|
for (const [byte] of value.matchAll(/[0-9a-fA-F]{2}/g)) {
|
||||||
|
bytes.push(parseInt(byte, 16));
|
||||||
|
}
|
||||||
|
return new Uint8Array(bytes);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user