Compare commits

..

2 Commits

Author SHA1 Message Date
4396f88762 feat: add Migu3D Keyless decipher 2024-09-20 23:23:19 +01:00
ddc9628305 feat: add qtfm decipher 2024-09-20 21:14:47 +01:00
15 changed files with 145 additions and 43 deletions

View File

@ -23,7 +23,7 @@
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@unlock-music/crypto": "0.0.0-alpha.15", "@unlock-music/crypto": "0.0.0-alpha.16",
"framer-motion": "^10.16.16", "framer-motion": "^10.16.16",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"radash": "^11.0.0", "radash": "^11.0.0",

View File

@ -39,8 +39,8 @@ importers:
specifier: ^2.0.1 specifier: ^2.0.1
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.16
version: 0.0.0-alpha.15 version: 0.0.0-alpha.16
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,8 +1921,8 @@ 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': '@unlock-music/crypto@0.0.0-alpha.16':
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} resolution: {integrity: sha512-QDhDWTsZOOkSQ64d3+pBwPCfesit6MiO63SzV7TQB8rQ/lPb78MMz6C4ECIjP9ijCBXds4PT4CBXp8vJDifAEg==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.16/crypto-0.0.0-alpha.16.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==}
@ -6110,7 +6110,7 @@ snapshots:
'@ungap/structured-clone@1.2.0': {} '@ungap/structured-clone@1.2.0': {}
'@unlock-music/crypto@0.0.0-alpha.15': {} '@unlock-music/crypto@0.0.0-alpha.16': {}
'@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:

View File

@ -6,6 +6,8 @@ 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';
import { Migu3DKeylessDecipher } from '~/decrypt-worker/decipher/Migu3d.ts';
export enum Status { export enum Status {
OK = 0, OK = 0,
@ -54,6 +56,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)
@ -63,7 +68,7 @@ export const allCryptoFactories: DecipherFactory[] = [
/// File without an obvious header or footer goes last. /// File without an obvious header or footer goes last.
// Migu3D/Keyless (*.wav; *.m4a) // Migu3D/Keyless (*.wav; *.m4a)
// MiguCrypto.make, Migu3DKeylessDecipher.make,
// Crypto that does not implement "checkBySignature" or need to decrypt the entire file and then check audio type, // Crypto that does not implement "checkBySignature" or need to decrypt the entire file and then check audio type,
// should be moved to the bottom of the list for performance reasons. // should be moved to the bottom of the list for performance reasons.
@ -75,9 +80,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,
]; ];

View File

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

View File

@ -0,0 +1,27 @@
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts';
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
import { Migu3D } from '@unlock-music/crypto';
export class Migu3DKeylessDecipher implements DecipherInstance {
cipherName = 'Migu3D (Keyless)';
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
const mg3d = Migu3D.fromHeader(buffer.subarray(0, 0x100));
const audioBuffer = new Uint8Array(buffer);
for (const [block, i] of chunkBuffer(audioBuffer)) {
mg3d.decrypt(block, i);
}
mg3d.free();
return {
cipherName: this.cipherName,
status: Status.OK,
data: audioBuffer,
};
}
public static make() {
return new Migu3DKeylessDecipher();
}
}

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; id: string;
blobURI: 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 { 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);

View File

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

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 { timedLogger } from '~/util/logUtils.ts';
import { QMCFooter } from '@unlock-music/crypto'; 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 { 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}`);
});
} }
}; };

View File

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

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