Compare commits
2 Commits
6f229ff39e
...
4396f88762
Author | SHA1 | Date | |
---|---|---|---|
4396f88762 | |||
ddc9628305 |
@ -23,7 +23,7 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@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",
|
||||
"nanoid": "^5.0.4",
|
||||
"radash": "^11.0.0",
|
||||
|
@ -39,8 +39,8 @@ importers:
|
||||
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)
|
||||
'@unlock-music/crypto':
|
||||
specifier: 0.0.0-alpha.15
|
||||
version: 0.0.0-alpha.15
|
||||
specifier: 0.0.0-alpha.16
|
||||
version: 0.0.0-alpha.16
|
||||
framer-motion:
|
||||
specifier: ^10.16.16
|
||||
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':
|
||||
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}
|
||||
'@unlock-music/crypto@0.0.0-alpha.16':
|
||||
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':
|
||||
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
|
||||
@ -6110,7 +6110,7 @@ snapshots:
|
||||
|
||||
'@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))':
|
||||
dependencies:
|
||||
|
@ -6,6 +6,8 @@ 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';
|
||||
import { Migu3DKeylessDecipher } from '~/decrypt-worker/decipher/Migu3d.ts';
|
||||
|
||||
export enum Status {
|
||||
OK = 0,
|
||||
@ -54,6 +56,9 @@ export const allCryptoFactories: DecipherFactory[] = [
|
||||
// Xiami (*.xm)
|
||||
XiamiDecipher.make,
|
||||
|
||||
// QingTingFM Android (*.qta)
|
||||
QignTingFMDecipher.make,
|
||||
|
||||
/// File with a fixed footer goes second
|
||||
|
||||
// QMCv2 (*.mflac)
|
||||
@ -63,7 +68,7 @@ export const allCryptoFactories: DecipherFactory[] = [
|
||||
/// File without an obvious header or footer goes last.
|
||||
|
||||
// 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,
|
||||
// should be moved to the bottom of the list for performance reasons.
|
||||
@ -75,9 +80,6 @@ export const allCryptoFactories: DecipherFactory[] = [
|
||||
XimalayaAndroidDecipher.makeX2M,
|
||||
XimalayaAndroidDecipher.makeX3M,
|
||||
|
||||
// QingTingFM (Android)
|
||||
// QingTingFM$Device.make,
|
||||
|
||||
// Transparent crypto (not encrypted)
|
||||
TransparentDecipher.make,
|
||||
];
|
||||
|
@ -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',
|
||||
}
|
||||
|
||||
|
27
src/decrypt-worker/decipher/Migu3d.ts
Normal file
27
src/decrypt-worker/decipher/Migu3d.ts
Normal 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();
|
||||
}
|
||||
}
|
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;
|
||||
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 { 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);
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
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 { 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}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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