Upgrade to use @unlock-music/crypto #78
@ -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.16",
|
"@unlock-music/crypto": "0.0.0-alpha.18",
|
||||||
"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",
|
||||||
|
@ -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.16
|
specifier: 0.0.0-alpha.18
|
||||||
version: 0.0.0-alpha.16
|
version: 0.0.0-alpha.18
|
||||||
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.16':
|
'@unlock-music/crypto@0.0.0-alpha.18':
|
||||||
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}
|
resolution: {integrity: sha512-/2VgU15WCW+GnWOgYd18GylQTkCGgAW7cf/qBcjXWU+2qtsvv9heiN8LkIWWz+NMbO0XDRONXijAKdKYf38o/g==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.18/crypto-0.0.0-alpha.18.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.16': {}
|
'@unlock-music/crypto@0.0.0-alpha.18': {}
|
||||||
|
|
||||||
'@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:
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
|
||||||
import { strlen } from './strlen';
|
|
||||||
|
|
||||||
export interface KuwoHeader {
|
|
||||||
rid: string; // uint64
|
|
||||||
encVersion: 1 | 2; // uint32
|
|
||||||
quality: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KUWO_MAGIC_HDRS = new Set(['yeelion-kuwo\x00\x00\x00\x00', 'yeelion-kuwo-tme']);
|
|
||||||
|
|
||||||
export function parseKuwoHeader(view: DataView): KuwoHeader | null {
|
|
||||||
const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10);
|
|
||||||
if (!KUWO_MAGIC_HDRS.has(bytesToUTF8String(magic))) {
|
|
||||||
return null; // not kuwo-encrypted file
|
|
||||||
}
|
|
||||||
|
|
||||||
const qualityBytes = new Uint8Array(view.buffer.slice(view.byteOffset + 0x30, view.byteOffset + 0x40));
|
|
||||||
const qualityLen = strlen(qualityBytes);
|
|
||||||
const quality = bytesToUTF8String(qualityBytes.slice(0, qualityLen));
|
|
||||||
|
|
||||||
return {
|
|
||||||
encVersion: view.getUint32(0x10, true) as 1 | 2,
|
|
||||||
rid: view.getUint32(0x18, true).toString(),
|
|
||||||
quality,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
export function strlen(data: Uint8Array): number {
|
|
||||||
const n = data.byteLength;
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
if (data[i] === 0) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
@ -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',
|
||||||
|
KUWO_PARSE_HEADER = 'KUWO_PARSE_HEADER',
|
||||||
QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY',
|
QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY',
|
||||||
VERSION = 'VERSION',
|
VERSION = 'VERSION',
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,18 @@ export interface DecryptCommandPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchMusicExNamePayload {
|
export interface FetchMusicExNamePayload {
|
||||||
id: string;
|
|
||||||
blobURI: string;
|
blobURI: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParseKuwoHeaderPayload {
|
||||||
|
blobURI: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParseKuwoHeaderResponse = null | {
|
||||||
|
resourceId: number;
|
||||||
|
qualityId: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface GetQingTingFMDeviceKeyPayload {
|
export interface GetQingTingFMDeviceKeyPayload {
|
||||||
product: string;
|
product: string;
|
||||||
device: string;
|
device: string;
|
||||||
|
@ -5,6 +5,7 @@ import { getUmcVersion } from '@unlock-music/crypto';
|
|||||||
import { workerDecryptHandler } from './worker/decrypt.ts';
|
import { workerDecryptHandler } from './worker/decrypt.ts';
|
||||||
import { workerParseMusicExMediaName } from './worker/qmcv2_parser.ts';
|
import { workerParseMusicExMediaName } from './worker/qmcv2_parser.ts';
|
||||||
import { workerGetQtfmDeviceKey } from '~/decrypt-worker/worker/qtfm_device_key.ts';
|
import { workerGetQtfmDeviceKey } from '~/decrypt-worker/worker/qtfm_device_key.ts';
|
||||||
|
import { workerParseKuwoHeader } from '~/decrypt-worker/worker/kuwo_header_parse.ts';
|
||||||
|
|
||||||
const bus = new WorkerServerBus();
|
const bus = new WorkerServerBus();
|
||||||
onmessage = bus.onmessage;
|
onmessage = bus.onmessage;
|
||||||
@ -12,4 +13,5 @@ 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.KUWO_PARSE_HEADER, workerParseKuwoHeader);
|
||||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey);
|
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey);
|
||||||
|
17
src/decrypt-worker/worker/kuwo_header_parse.ts
Normal file
17
src/decrypt-worker/worker/kuwo_header_parse.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { FetchMusicExNamePayload, ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts';
|
||||||
|
import { KuwoHeader } from '@unlock-music/crypto';
|
||||||
|
|
||||||
|
export const workerParseKuwoHeader = async ({ blobURI }: FetchMusicExNamePayload): Promise<ParseKuwoHeaderResponse> => {
|
||||||
|
const blob = await fetch(blobURI, { headers: { Range: 'bytes=0-1023' } }).then((r) => r.blob());
|
||||||
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = new Uint8Array(arrayBuffer.slice(0, 1024));
|
||||||
|
const kwm = KuwoHeader.parse(buffer);
|
||||||
|
const { qualityId, resourceId } = kwm;
|
||||||
|
kwm.free();
|
||||||
|
return { qualityId, resourceId };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
@ -1,10 +1,7 @@
|
|||||||
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types.ts';
|
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types.ts';
|
||||||
import { timedLogger } from '~/util/logUtils.ts';
|
|
||||||
import { QMCFooter } from '@unlock-music/crypto';
|
import { QMCFooter } from '@unlock-music/crypto';
|
||||||
|
|
||||||
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
|
export const workerParseMusicExMediaName = async ({ blobURI }: FetchMusicExNamePayload) => {
|
||||||
const label = `qmcMusixExDetectName(${id.replace('://', ':')})`;
|
|
||||||
return timedLogger(label, async () => {
|
|
||||||
const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob());
|
const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob());
|
||||||
const arrayBuffer = await blob.arrayBuffer();
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
|
|
||||||
@ -15,5 +12,4 @@ export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExN
|
|||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||||
import type { RootState } from '~/store';
|
import type { RootState } from '~/store';
|
||||||
|
|
||||||
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
||||||
import type { DecryptCommandOptions, FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
import type {
|
||||||
|
DecryptCommandOptions,
|
||||||
|
FetchMusicExNamePayload,
|
||||||
|
ParseKuwoHeaderPayload,
|
||||||
|
ParseKuwoHeaderResponse,
|
||||||
|
} from '~/decrypt-worker/types';
|
||||||
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
||||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||||
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
import { selectKWMv2Key, selectQMCv2KeyByFileName, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||||
|
|
||||||
export enum ProcessState {
|
export enum ProcessState {
|
||||||
QUEUED = 'QUEUED',
|
QUEUED = 'QUEUED',
|
||||||
@ -43,6 +48,7 @@ export interface FileListingState {
|
|||||||
files: Record<string, DecryptedAudioFile>;
|
files: Record<string, DecryptedAudioFile>;
|
||||||
displayMode: ListingMode;
|
displayMode: ListingMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: FileListingState = {
|
const initialState: FileListingState = {
|
||||||
files: {},
|
files: {},
|
||||||
displayMode: ListingMode.LIST,
|
displayMode: ListingMode.LIST,
|
||||||
@ -64,28 +70,20 @@ export const processFile = createAsyncThunk<
|
|||||||
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } })
|
const [qmcv2MusicExMediaFile, kuwoHdr] = await Promise.all([
|
||||||
.then((r) => r.blob())
|
workerClientBus.request<string, FetchMusicExNamePayload>(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, {
|
||||||
.then((r) => r.arrayBuffer())
|
|
||||||
.then((r) => {
|
|
||||||
if (r.byteLength > 1024) {
|
|
||||||
return r.slice(0, 1024);
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
|
|
||||||
const qmcv2MusicExMediaFile = await workerClientBus.request<string, FetchMusicExNamePayload>(
|
|
||||||
DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME,
|
|
||||||
{
|
|
||||||
id: fileId,
|
|
||||||
blobURI: file.raw,
|
blobURI: file.raw,
|
||||||
},
|
}),
|
||||||
);
|
workerClientBus.request<ParseKuwoHeaderResponse, ParseKuwoHeaderPayload>(
|
||||||
|
DECRYPTION_WORKER_ACTION_NAME.KUWO_PARSE_HEADER,
|
||||||
|
{ blobURI: file.raw },
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
const options: DecryptCommandOptions = {
|
const options: DecryptCommandOptions = {
|
||||||
fileName: file.fileName,
|
fileName: file.fileName,
|
||||||
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
|
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
|
||||||
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
kwm2key: selectKWMv2Key(state, kuwoHdr),
|
||||||
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||||
};
|
};
|
||||||
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { parseKuwoHeader } from '~/crypto/parseKuwo';
|
|
||||||
import type { RootState } from '~/store';
|
import type { RootState } from '~/store';
|
||||||
import { closestByLevenshtein } from '~/util/levenshtein';
|
import { closestByLevenshtein } from '~/util/levenshtein';
|
||||||
import { hasOwn } from '~/util/objects';
|
import { hasOwn } from '~/util/objects';
|
||||||
import { kwm2StagingToProductionKey } from './keyFormats';
|
import { kwm2StagingToProductionKey } from './keyFormats';
|
||||||
|
import type { ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts';
|
||||||
|
|
||||||
export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty;
|
export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty;
|
||||||
|
|
||||||
@ -31,14 +31,16 @@ export const selectQMCv2KeyByFileName = (state: RootState, name: string): string
|
|||||||
return ekey;
|
return ekey;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectKWMv2Key = (state: RootState, headerView: DataView): string | undefined => {
|
export const selectKWMv2Key = (state: RootState, hdr: ParseKuwoHeaderResponse): string | undefined => {
|
||||||
const hdr = parseKuwoHeader(headerView);
|
|
||||||
if (!hdr) {
|
if (!hdr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quality = String(hdr.qualityId);
|
||||||
|
const rid = String(hdr.resourceId);
|
||||||
|
|
||||||
const keys = selectFinalKWMv2Keys(state);
|
const keys = selectFinalKWMv2Keys(state);
|
||||||
const lookupKey = kwm2StagingToProductionKey({ id: '', ekey: '', quality: hdr.quality, rid: hdr.rid });
|
const lookupKey = kwm2StagingToProductionKey({ id: '', ekey: '', quality, rid });
|
||||||
|
|
||||||
let ekey: string | undefined;
|
let ekey: string | undefined;
|
||||||
if (hasOwn(keys, lookupKey)) {
|
if (hasOwn(keys, lookupKey)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user