diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4265975..8eb7f5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) '@unlock-music/crypto': specifier: 0.0.0-alpha.15 - version: 0.0.0-alpha.15 + version: link:../lib_um_crypto_rust/um_wasm_loader framer-motion: specifier: ^10.16.16 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': 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': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6110,8 +6107,6 @@ snapshots: '@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))': dependencies: '@babel/core': 7.23.6 diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index 5aef2b1..d11da39 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -6,6 +6,7 @@ import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts'; import { XiamiDecipher } from '~/decrypt-worker/decipher/XiamiMusic.ts'; +import { QignTingFMDecipher } from '~/decrypt-worker/decipher/QingTingFM.ts'; export enum Status { OK = 0, @@ -54,6 +55,9 @@ export const allCryptoFactories: DecipherFactory[] = [ // Xiami (*.xm) XiamiDecipher.make, + // QingTingFM Android (*.qta) + QignTingFMDecipher.make, + /// File with a fixed footer goes second // QMCv2 (*.mflac) @@ -75,9 +79,6 @@ export const allCryptoFactories: DecipherFactory[] = [ XimalayaAndroidDecipher.makeX2M, XimalayaAndroidDecipher.makeX3M, - // QingTingFM (Android) - // QingTingFM$Device.make, - // Transparent crypto (not encrypted) TransparentDecipher.make, ]; diff --git a/src/decrypt-worker/constants.ts b/src/decrypt-worker/constants.ts index 643d4ae..5a9bdce 100644 --- a/src/decrypt-worker/constants.ts +++ b/src/decrypt-worker/constants.ts @@ -1,6 +1,7 @@ export enum DECRYPTION_WORKER_ACTION_NAME { DECRYPT = 'DECRYPT', FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME', + QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY', VERSION = 'VERSION', } diff --git a/src/decrypt-worker/decipher/QingTingFM.ts b/src/decrypt-worker/decipher/QingTingFM.ts new file mode 100644 index 0000000..f128e65 --- /dev/null +++ b/src/decrypt-worker/decipher/QingTingFM.ts @@ -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 { + 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(); + } +} diff --git a/src/decrypt-worker/types.ts b/src/decrypt-worker/types.ts index 9157ed0..f83826d 100644 --- a/src/decrypt-worker/types.ts +++ b/src/decrypt-worker/types.ts @@ -15,3 +15,12 @@ export interface FetchMusicExNamePayload { id: string; blobURI: string; } + +export interface GetQingTingFMDeviceKeyPayload { + product: string; + device: string; + manufacturer: string; + brand: string; + board: string; + model: string; +} diff --git a/src/decrypt-worker/worker.ts b/src/decrypt-worker/worker.ts index 1c5c712..600bb91 100644 --- a/src/decrypt-worker/worker.ts +++ b/src/decrypt-worker/worker.ts @@ -2,8 +2,9 @@ import { WorkerServerBus } from '~/util/WorkerEventBus'; import { DECRYPTION_WORKER_ACTION_NAME } from './constants'; import { getUmcVersion } from '@unlock-music/crypto'; -import { workerDecryptHandler } from './worker/handler/decrypt'; -import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser'; +import { workerDecryptHandler } from './worker/decrypt.ts'; +import { workerParseMusicExMediaName } from './worker/qmcv2_parser.ts'; +import { workerGetQtfmDeviceKey } from '~/decrypt-worker/worker/qtfm_device_key.ts'; const bus = new WorkerServerBus(); onmessage = bus.onmessage; @@ -11,3 +12,4 @@ onmessage = bus.onmessage; 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.VERSION, getUmcVersion); +bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey); diff --git a/src/decrypt-worker/worker/handler/decrypt.ts b/src/decrypt-worker/worker/decrypt.ts similarity index 91% rename from src/decrypt-worker/worker/handler/decrypt.ts rename to src/decrypt-worker/worker/decrypt.ts index 18d7727..5f75690 100644 --- a/src/decrypt-worker/worker/handler/decrypt.ts +++ b/src/decrypt-worker/worker/decrypt.ts @@ -1,10 +1,10 @@ -import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils'; -import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types'; -import { allCryptoFactories } from '../../Deciphers.ts'; -import { toBlob } from '~/decrypt-worker/util/buffer'; +import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils.ts'; +import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types.ts'; +import { allCryptoFactories } from '../Deciphers.ts'; +import { toBlob } from '~/decrypt-worker/util/buffer.ts'; import { DecipherFactory, DecipherInstance, Status } from '~/decrypt-worker/Deciphers.ts'; -import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError'; -import { ready as umCryptoReady } from '@unlock-music/crypto'; +import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; +import { ready as umCryptoReady } from '../../../../lib_um_crypto_rust/um_wasm_loader'; import { go } from '~/util/go.ts'; import { detectAudioExtension } from '~/decrypt-worker/util/audioType.ts'; diff --git a/src/decrypt-worker/worker/handler/qmcv2_parser.ts b/src/decrypt-worker/worker/qmcv2_parser.ts similarity index 98% rename from src/decrypt-worker/worker/handler/qmcv2_parser.ts rename to src/decrypt-worker/worker/qmcv2_parser.ts index aea0098..0b9d8d2 100644 --- a/src/decrypt-worker/worker/handler/qmcv2_parser.ts +++ b/src/decrypt-worker/worker/qmcv2_parser.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 { QMCFooter } from '@unlock-music/crypto'; diff --git a/src/decrypt-worker/worker/qtfm_device_key.ts b/src/decrypt-worker/worker/qtfm_device_key.ts new file mode 100644 index 0000000..03a7730 --- /dev/null +++ b/src/decrypt-worker/worker/qtfm_device_key.ts @@ -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); +} diff --git a/src/features/settings/panels/PanelQingTing.tsx b/src/features/settings/panels/PanelQingTing.tsx index 3089526..80ca0d8 100644 --- a/src/features/settings/panels/PanelQingTing.tsx +++ b/src/features/settings/panels/PanelQingTing.tsx @@ -18,6 +18,9 @@ import { ChangeEvent, ClipboardEvent } from 'react'; import { VQuote } from '~/components/HelpText/VQuote'; import { selectStagingQtfmAndroidKey } from '../settingsSelector'; 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'; @@ -37,29 +40,23 @@ export function PanelQingTing() { return; } - const dataMap = new Map(); - for (const [_unused, key, value] of plainText.matchAll( - /^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim, - )) { - dataMap.set(key.toLowerCase(), value); + const dataMap = Object.create(null); + for (const [, key, value] of plainText.matchAll(/^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim)) { + dataMap[key.toLowerCase()] = value; } + const { product, device, manufacturer, brand, board, model } = dataMap; - const product = dataMap.get('product') ?? null; - 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 - ) { + if (product && device && manufacturer && brand && board && model) { e.preventDefault(); - alert('TODO!'); + workerClientBus + .request( + DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, + dataMap, + ) + .then(setSecretKey) + .catch((err) => { + alert(`生成设备密钥时发生错误: ${err}`); + }); } }; diff --git a/src/util/MMKVParser.ts b/src/util/MMKVParser.ts index bd9b2b6..9525df7 100644 --- a/src/util/MMKVParser.ts +++ b/src/util/MMKVParser.ts @@ -1,5 +1,5 @@ import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder'; -import { formatHex } from './formatHex'; +import { formatHex } from './hex.ts'; export class MMKVParser { private offset = 4; diff --git a/src/util/formatHex.ts b/src/util/formatHex.ts deleted file mode 100644 index 0d778c1..0000000 --- a/src/util/formatHex.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function formatHex(value: number, len = 8) { - return '0x' + (value | 0).toString(16).padStart(len, '0'); -} diff --git a/src/util/hex.ts b/src/util/hex.ts new file mode 100644 index 0000000..897fdc0 --- /dev/null +++ b/src/util/hex.ts @@ -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); +}