feat: add qtfm decipher

This commit is contained in:
鲁树人 2024-09-20 21:14:47 +01:00
parent 6f229ff39e
commit ddc9628305
13 changed files with 111 additions and 42 deletions

View File

@ -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

View File

@ -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,
];

View File

@ -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',
}

View 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();
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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';

View File

@ -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';

View 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);
}

View File

@ -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<string, GetQingTingFMDeviceKeyPayload>(
DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY,
dataMap,
)
.then(setSecretKey)
.catch((err) => {
alert(`生成设备密钥时发生错误: ${err}`);
});
}
};

View File

@ -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;

View File

@ -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
View 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);
}