Upgrade to use @unlock-music/crypto #78

Merged
lsr merged 18 commits from upgrade/um-crypto-part1 into main 2024-09-24 22:19:32 +00:00
11 changed files with 69 additions and 81 deletions
Showing only changes of commit d071f0bd65 - Show all commits

View File

@ -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.16",
"@unlock-music/crypto": "0.0.0-alpha.18",
"framer-motion": "^10.16.16",
"nanoid": "^5.0.4",
"radash": "^11.0.0",

View File

@ -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.16
version: 0.0.0-alpha.16
specifier: 0.0.0-alpha.18
version: 0.0.0-alpha.18
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.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}
'@unlock-music/crypto@0.0.0-alpha.18':
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':
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.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))':
dependencies:

View File

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

View File

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

View File

@ -1,6 +1,7 @@
export enum DECRYPTION_WORKER_ACTION_NAME {
DECRYPT = 'DECRYPT',
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
KUWO_PARSE_HEADER = 'KUWO_PARSE_HEADER',
QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY',
VERSION = 'VERSION',
}

View File

@ -12,10 +12,18 @@ export interface DecryptCommandPayload {
}
export interface FetchMusicExNamePayload {
id: string;
blobURI: string;
}
export interface ParseKuwoHeaderPayload {
blobURI: string;
}
export type ParseKuwoHeaderResponse = null | {
resourceId: number;
qualityId: number;
};
export interface GetQingTingFMDeviceKeyPayload {
product: string;
device: string;

View File

@ -5,6 +5,7 @@ import { getUmcVersion } from '@unlock-music/crypto';
import { workerDecryptHandler } from './worker/decrypt.ts';
import { workerParseMusicExMediaName } from './worker/qmcv2_parser.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();
onmessage = bus.onmessage;
@ -12,4 +13,5 @@ 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.KUWO_PARSE_HEADER, workerParseKuwoHeader);
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey);

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

View File

@ -1,10 +1,7 @@
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types.ts';
import { timedLogger } from '~/util/logUtils.ts';
import { QMCFooter } from '@unlock-music/crypto';
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
const label = `qmcMusixExDetectName(${id.replace('://', ':')})`;
return timedLogger(label, async () => {
export const workerParseMusicExMediaName = async ({ blobURI }: FetchMusicExNamePayload) => {
const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob());
const arrayBuffer = await blob.arrayBuffer();
@ -15,5 +12,4 @@ export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExN
} catch {
return null;
}
});
};

View File

@ -1,12 +1,17 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import type { RootState } from '~/store';
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 { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
import { selectKWMv2Key, selectQMCv2KeyByFileName, selectQtfmAndroidKey } from '../settings/settingsSelector';
export enum ProcessState {
QUEUED = 'QUEUED',
@ -43,6 +48,7 @@ export interface FileListingState {
files: Record<string, DecryptedAudioFile>;
displayMode: ListingMode;
}
const initialState: FileListingState = {
files: {},
displayMode: ListingMode.LIST,
@ -64,28 +70,20 @@ export const processFile = createAsyncThunk<
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
};
const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } })
.then((r) => r.blob())
.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,
const [qmcv2MusicExMediaFile, kuwoHdr] = await Promise.all([
workerClientBus.request<string, FetchMusicExNamePayload>(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, {
blobURI: file.raw,
},
);
}),
workerClientBus.request<ParseKuwoHeaderResponse, ParseKuwoHeaderPayload>(
DECRYPTION_WORKER_ACTION_NAME.KUWO_PARSE_HEADER,
{ blobURI: file.raw },
),
]);
const options: DecryptCommandOptions = {
fileName: file.fileName,
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
kwm2key: selectKWMv2Key(state, kuwoHdr),
qingTingAndroidKey: selectQtfmAndroidKey(state),
};
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);

View File

@ -1,8 +1,8 @@
import { parseKuwoHeader } from '~/crypto/parseKuwo';
import type { RootState } from '~/store';
import { closestByLevenshtein } from '~/util/levenshtein';
import { hasOwn } from '~/util/objects';
import { kwm2StagingToProductionKey } from './keyFormats';
import type { ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts';
export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty;
@ -31,14 +31,16 @@ export const selectQMCv2KeyByFileName = (state: RootState, name: string): string
return ekey;
};
export const selectKWMv2Key = (state: RootState, headerView: DataView): string | undefined => {
const hdr = parseKuwoHeader(headerView);
export const selectKWMv2Key = (state: RootState, hdr: ParseKuwoHeaderResponse): string | undefined => {
if (!hdr) {
return;
}
const quality = String(hdr.qualityId);
const rid = String(hdr.resourceId);
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;
if (hasOwn(keys, lookupKey)) {