refactor: add qmc and kuwo from @unlock-music/crypto
This commit is contained in:
parent
8b416f8055
commit
a75ca7aabb
@ -24,7 +24,7 @@
|
|||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@um/libparakeet": "0.4.5",
|
"@um/libparakeet": "0.4.5",
|
||||||
"@unlock-music/crypto": "0.0.0-alpha.10",
|
"@unlock-music/crypto": "0.0.0-alpha.11",
|
||||||
"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",
|
||||||
|
@ -42,8 +42,8 @@ importers:
|
|||||||
specifier: 0.4.5
|
specifier: 0.4.5
|
||||||
version: 0.4.5
|
version: 0.4.5
|
||||||
'@unlock-music/crypto':
|
'@unlock-music/crypto':
|
||||||
specifier: 0.0.0-alpha.10
|
specifier: 0.0.0-alpha.11
|
||||||
version: 0.0.0-alpha.10
|
version: 0.0.0-alpha.11
|
||||||
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)
|
||||||
@ -1927,8 +1927,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.10':
|
'@unlock-music/crypto@0.0.0-alpha.11':
|
||||||
resolution: {integrity: sha512-Y8PWd/f4KEh2WU5Uz4QesnYMelDvioLYMOVvpPceMk62P2LivQLgvl5+ytDmD2yzmsnE/sXOQztTg+1WsuePCA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.10/crypto-0.0.0-alpha.10.tgz}
|
resolution: {integrity: sha512-lA3xryziHULhkPbuQFI2HrfwDREUD9YoaZOTMQqcu/8mKF2/hA3sCK0Uoq0miYr+7VUbE5sMBvl9dcrnCI1UWA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.11/crypto-0.0.0-alpha.11.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==}
|
||||||
@ -6118,7 +6118,7 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.2.0': {}
|
'@ungap/structured-clone@1.2.0': {}
|
||||||
|
|
||||||
'@unlock-music/crypto@0.0.0-alpha.10': {}
|
'@unlock-music/crypto@0.0.0-alpha.11': {}
|
||||||
|
|
||||||
'@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,6 +1,8 @@
|
|||||||
import { NetEaseCloudMusicDecipher } from '~/decrypt-worker/decipher/NetEaseCloudMusic.ts';
|
import { NetEaseCloudMusicDecipher } from '~/decrypt-worker/decipher/NetEaseCloudMusic.ts';
|
||||||
import { TransparentDecipher } from './decipher/Transparent.ts';
|
import { TransparentDecipher } from './decipher/Transparent.ts';
|
||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||||
|
import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts';
|
||||||
|
import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts';
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
OK = 0,
|
OK = 0,
|
||||||
@ -41,7 +43,7 @@ export const allCryptoFactories: DecipherFactory[] = [
|
|||||||
// KGMCrypto.make,
|
// KGMCrypto.make,
|
||||||
|
|
||||||
// KWMv1 (*.kwm)
|
// KWMv1 (*.kwm)
|
||||||
// KWMCrypto.make,
|
KuwoMusicDecipher.make,
|
||||||
|
|
||||||
// Xiami (*.xm)
|
// Xiami (*.xm)
|
||||||
// XiamiCrypto.make,
|
// XiamiCrypto.make,
|
||||||
@ -49,8 +51,8 @@ export const allCryptoFactories: DecipherFactory[] = [
|
|||||||
/// File with a fixed footer goes second
|
/// File with a fixed footer goes second
|
||||||
|
|
||||||
// QMCv2 (*.mflac)
|
// QMCv2 (*.mflac)
|
||||||
// QMC2CryptoWithKey.make,
|
QQMusicV2Decipher.createWithUserKey,
|
||||||
// QMC2Crypto.make,
|
QQMusicV2Decipher.createWithEmbeddedEKey,
|
||||||
|
|
||||||
/// File without an obvious header or footer goes last.
|
/// File without an obvious header or footer goes last.
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ export const allCryptoFactories: DecipherFactory[] = [
|
|||||||
// should be moved to the bottom of the list for performance reasons.
|
// should be moved to the bottom of the list for performance reasons.
|
||||||
|
|
||||||
// QMCv1 (*.qmcflac)
|
// QMCv1 (*.qmcflac)
|
||||||
// QMC1Crypto.make,
|
QQMusicV1Decipher.create,
|
||||||
|
|
||||||
// Ximalaya (Android)
|
// Ximalaya (Android)
|
||||||
// XimalayaAndroidCrypto.makeX2M,
|
// XimalayaAndroidCrypto.makeX2M,
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
|
||||||
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
|
|
||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
|
||||||
import { fetchParakeet } from '@um/libparakeet';
|
|
||||||
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts';
|
|
||||||
import { makeQMCv2FooterParser, makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts';
|
|
||||||
|
|
||||||
export class QMC2Crypto implements DecipherInstance {
|
|
||||||
cryptoName = 'QMC/v2';
|
|
||||||
checkByDecryptHeader = false;
|
|
||||||
|
|
||||||
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
|
|
||||||
const parakeet = await fetchParakeet();
|
|
||||||
const footerParser = makeQMCv2FooterParser(parakeet);
|
|
||||||
return transformBlob(buffer, (p) => p.make.QMCv2(footerParser), {
|
|
||||||
parakeet,
|
|
||||||
cleanup: () => footerParser.delete(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static make() {
|
|
||||||
return new QMC2Crypto();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QMC2CryptoWithKey implements DecipherInstance {
|
|
||||||
cryptoName = 'QMC/v2 (key)';
|
|
||||||
checkByDecryptHeader = true;
|
|
||||||
|
|
||||||
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<boolean> {
|
|
||||||
return Boolean(options.qmc2Key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
|
|
||||||
if (!options.qmc2Key) {
|
|
||||||
throw new Error('key was not provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parakeet = await fetchParakeet();
|
|
||||||
const key = stringToUTF8Bytes(options.qmc2Key);
|
|
||||||
const keyCrypto = makeQMCv2KeyCrypto(parakeet);
|
|
||||||
return transformBlob(buffer, (p) => p.make.QMCv2EKey(key, keyCrypto), {
|
|
||||||
parakeet,
|
|
||||||
cleanup: () => keyCrypto.delete(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static make() {
|
|
||||||
return new QMC2CryptoWithKey();
|
|
||||||
}
|
|
||||||
}
|
|
35
src/decrypt-worker/decipher/KuwoMusic.ts
Normal file
35
src/decrypt-worker/decipher/KuwoMusic.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
||||||
|
import { KuwoHeader, KWMCipher } from '@unlock-music/crypto';
|
||||||
|
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||||
|
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||||
|
|
||||||
|
export class KuwoMusicDecipher implements DecipherInstance {
|
||||||
|
cipherName = 'Kuwo';
|
||||||
|
|
||||||
|
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||||
|
let header: KuwoHeader | undefined;
|
||||||
|
let kwm: KWMCipher | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
header = KuwoHeader.parse(buffer.subarray(0, 0x400));
|
||||||
|
kwm = header.makeCipher(options.kwm2key);
|
||||||
|
|
||||||
|
const audioBuffer = new Uint8Array(buffer.subarray(0x400));
|
||||||
|
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||||
|
kwm.decrypt(block, offset);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: Status.OK,
|
||||||
|
cipherName: this.cipherName,
|
||||||
|
data: audioBuffer,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
kwm?.free();
|
||||||
|
header?.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static make() {
|
||||||
|
return new KuwoMusicDecipher();
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
||||||
import { NCMFile } from '@unlock-music/crypto';
|
import { NCMFile } from '@unlock-music/crypto';
|
||||||
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||||
import { withWasmClass } from '~/decrypt-worker/util/wasmClass.ts';
|
|
||||||
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
|
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
|
||||||
|
|
||||||
export class NetEaseCloudMusicDecipher implements DecipherInstance {
|
export class NetEaseCloudMusicDecipher implements DecipherInstance {
|
||||||
@ -18,12 +17,12 @@ export class NetEaseCloudMusicDecipher implements DecipherInstance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async decrypt(buffer: ArrayBuffer): Promise<DecipherResult | DecipherOK> {
|
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
|
||||||
return withWasmClass(new NCMFile(), async (ncm): Promise<DecipherOK> => {
|
const ncm = new NCMFile();
|
||||||
const data = new Uint8Array(buffer);
|
try {
|
||||||
this.tryInit(ncm, data);
|
this.tryInit(ncm, buffer);
|
||||||
|
|
||||||
const audioBuffer = data.subarray(ncm.audioOffset);
|
const audioBuffer = buffer.slice(ncm.audioOffset);
|
||||||
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||||
ncm.decrypt(block, offset);
|
ncm.decrypt(block, offset);
|
||||||
}
|
}
|
||||||
@ -32,7 +31,9 @@ export class NetEaseCloudMusicDecipher implements DecipherInstance {
|
|||||||
cipherName: this.cipherName,
|
cipherName: this.cipherName,
|
||||||
data: audioBuffer,
|
data: audioBuffer,
|
||||||
};
|
};
|
||||||
});
|
} finally {
|
||||||
|
ncm.free();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static make() {
|
public static make() {
|
||||||
|
74
src/decrypt-worker/decipher/QQMusic.ts
Normal file
74
src/decrypt-worker/decipher/QQMusic.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
||||||
|
import { decryptQMC1, QMC2, QMCFooter } from '@unlock-music/crypto';
|
||||||
|
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||||
|
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||||
|
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
|
||||||
|
import { isDataLooksLikeAudio } from '~/decrypt-worker/util/audioType.ts';
|
||||||
|
|
||||||
|
export class QQMusicV1Decipher implements DecipherInstance {
|
||||||
|
cipherName = 'QQMusic/QMC1';
|
||||||
|
|
||||||
|
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
|
||||||
|
const header = buffer.slice(0, 0x20);
|
||||||
|
decryptQMC1(header, 0);
|
||||||
|
if (!isDataLooksLikeAudio(header)) {
|
||||||
|
throw new UnsupportedSourceFile('does not look like QMC file');
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioBuffer = new Uint8Array(buffer);
|
||||||
|
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||||
|
decryptQMC1(block, offset);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: Status.OK,
|
||||||
|
cipherName: this.cipherName,
|
||||||
|
data: audioBuffer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create() {
|
||||||
|
return new QQMusicV1Decipher();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QQMusicV2Decipher implements DecipherInstance {
|
||||||
|
cipherName: string;
|
||||||
|
|
||||||
|
constructor(private readonly useUserKey: boolean) {
|
||||||
|
this.cipherName = `QQMusic/QMC2(user_key=${+useUserKey})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||||
|
const footer = QMCFooter.parse(buffer.subarray(buffer.byteLength - 1024));
|
||||||
|
if (!footer) {
|
||||||
|
throw new UnsupportedSourceFile('Not QMC2 File');
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size);
|
||||||
|
const ekey = this.useUserKey ? options.qmc2Key : footer.ekey;
|
||||||
|
footer.free();
|
||||||
|
if (!ekey) {
|
||||||
|
throw new Error('EKey missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const qmc2 = new QMC2(ekey);
|
||||||
|
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||||
|
qmc2.decrypt(block, offset);
|
||||||
|
}
|
||||||
|
qmc2.free();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: Status.OK,
|
||||||
|
cipherName: this.cipherName,
|
||||||
|
data: audioBuffer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createWithUserKey() {
|
||||||
|
return new QQMusicV2Decipher(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createWithEmbeddedEKey() {
|
||||||
|
return new QQMusicV2Decipher(false);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { detectAudioType } from '@unlock-music/crypto';
|
import { detectAudioType } from '@unlock-music/crypto';
|
||||||
|
|
||||||
export async function detectAudioExtension(buffer: Uint8Array): Promise<string> {
|
export function detectAudioExtension(buffer: Uint8Array): string {
|
||||||
let neededLength = 0x100;
|
let neededLength = 0x100;
|
||||||
let extension = 'bin';
|
let extension = 'bin';
|
||||||
while (neededLength !== 0) {
|
while (neededLength !== 0) {
|
||||||
@ -12,3 +12,15 @@ export async function detectAudioExtension(buffer: Uint8Array): Promise<string>
|
|||||||
}
|
}
|
||||||
return extension;
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDataLooksLikeAudio(buffer: Uint8Array): boolean {
|
||||||
|
if (buffer.byteLength < 0x20) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectResult = detectAudioType(buffer.subarray(0, 0x20));
|
||||||
|
|
||||||
|
// If we have needMore != 0, that means we have a valid header (ID3 for example).
|
||||||
|
const ok = detectResult.needMore !== 0 || detectResult.audioType !== 'bin';
|
||||||
|
detectResult.free();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
@ -65,7 +65,7 @@ class DecryptCommandHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we had a successful decryption
|
// Check if we had a successful decryption
|
||||||
let audioExt = result.overrideExtension || (await detectAudioExtension(result.data));
|
let audioExt = result.overrideExtension || detectAudioExtension(result.data);
|
||||||
if (!result.overrideExtension && audioExt === 'bin') {
|
if (!result.overrideExtension && audioExt === 'bin') {
|
||||||
throw new UnsupportedSourceFile('unable to produce valid audio file');
|
throw new UnsupportedSourceFile('unable to produce valid audio file');
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
// Allow pnpm to link.
|
// Allow pnpm to link.
|
||||||
process.env.LIB_PARAKEET_JS_DIR || '../libparakeet-js',
|
process.env.LIB_PARAKEET_JS_DIR || '../libparakeet-js',
|
||||||
|
process.env.LIB_UM_WASM_LOADER_DIR || '../lib_um_crypto_rust/um_wasm_loader',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user