Compare commits

...

3 Commits

Author SHA1 Message Date
c3809b48f7 faet: add support of xmly android (#1) 2023-05-21 23:38:50 +01:00
fa2629ae6c refactor: improve flow of decryption (#2) 2023-05-21 23:38:32 +01:00
e6bc60b9af chore: bump sdk version 2023-05-21 23:37:30 +01:00
11 changed files with 99 additions and 26 deletions

View File

@ -20,7 +20,7 @@
"@chakra-ui/react": "^2.6.1", "@chakra-ui/react": "^2.6.1",
"@emotion/react": "^11.11.0", "@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@jixun/libparakeet": "0.0.0-exp.16", "@jixun/libparakeet": "0.0.0-exp.17",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",

View File

@ -14,8 +14,8 @@ dependencies:
specifier: ^11.11.0 specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.0.28)(react@18.2.0) version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.0.28)(react@18.2.0)
'@jixun/libparakeet': '@jixun/libparakeet':
specifier: 0.0.0-exp.16 specifier: 0.0.0-exp.17
version: 0.0.0-exp.16 version: 0.0.0-exp.17
'@reduxjs/toolkit': '@reduxjs/toolkit':
specifier: ^1.9.5 specifier: ^1.9.5
version: 1.9.5(react-redux@8.0.5)(react@18.2.0) version: 1.9.5(react-redux@8.0.5)(react@18.2.0)
@ -2022,9 +2022,9 @@ packages:
chalk: 4.1.2 chalk: 4.1.2
dev: true dev: true
/@jixun/libparakeet@0.0.0-exp.16: /@jixun/libparakeet@0.0.0-exp.17:
resolution: resolution:
{ integrity: sha512-jDj9kju0tCJyesV+yi4xcOGClrFgdarkcG/aGP3tRfonf1l8y11A8lVwbp3cWXJcrgHAn7683rTEUYitg9TfpA== } { integrity: sha512-DazwsjE8KDNo3gHDILozgNMQwvQXY2N5CdXKvIMMV4xFOnEBVbJRaG5r7rmGoWnplQZebQAW+otwyVUtO1/ZdQ== }
dev: false dev: false
/@jridgewell/gen-mapping@0.3.3: /@jridgewell/gen-mapping@0.3.3:

View File

@ -1,7 +1,12 @@
export interface CryptoBase { export interface CryptoBase {
cryptoName: string; cryptoName: string;
checkByDecryptHeader: boolean; checkByDecryptHeader: boolean;
decryptTargetAudio: boolean;
/**
* If set, this new extension will be used instead.
* Useful for non-audio format, e.g. qrc to lrc/xml.
*/
overrideExtension?: string;
checkBySignature?: (buffer: ArrayBuffer) => Promise<boolean>; checkBySignature?: (buffer: ArrayBuffer) => Promise<boolean>;
decrypt(buffer: ArrayBuffer, blob: Blob): Promise<Blob | ArrayBuffer>; decrypt(buffer: ArrayBuffer, blob: Blob): Promise<Blob | ArrayBuffer>;

View File

@ -3,17 +3,22 @@ import { CryptoFactory } from './CryptoBase';
import { QMC1Crypto } from './qmc/qmc_v1'; import { QMC1Crypto } from './qmc/qmc_v1';
import { QMC2Crypto } from './qmc/qmc_v2'; import { QMC2Crypto } from './qmc/qmc_v2';
import { XiamiCrypto } from './xiami/xiami'; import { XiamiCrypto } from './xiami/xiami';
import { XimalayaAndroidCrypto } from './xmly/xmly_android';
export const allCryptoFactories: CryptoFactory[] = [ export const allCryptoFactories: CryptoFactory[] = [
// Xiami (*.xm) // Xiami (*.xm)
() => new XiamiCrypto(), XiamiCrypto.make,
// QMCv2 (*.mflac) // QMCv2 (*.mflac)
() => new QMC2Crypto(), QMC2Crypto.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.
// QMCv1 (*.qmcflac) // QMCv1 (*.qmcflac)
() => new QMC1Crypto(), QMC1Crypto.make,
// Ximalaya (Android)
XimalayaAndroidCrypto.makeX2M,
XimalayaAndroidCrypto.makeX3M,
]; ];

View File

@ -3,11 +3,14 @@ import type { CryptoBase } from '../CryptoBase';
import key from './qmc_v1.key.ts'; import key from './qmc_v1.key.ts';
export class QMC1Crypto implements CryptoBase { export class QMC1Crypto implements CryptoBase {
cryptoName = 'QMCv1'; cryptoName = 'QMC/v1';
checkByDecryptHeader = true; checkByDecryptHeader = true;
decryptTargetAudio = true;
async decrypt(_buffer: ArrayBuffer, blob: Blob): Promise<Blob> { async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(blob, (p) => p.make.QMCv1(key)); return transformBlob(buffer, (p) => p.make.QMCv1(key));
}
public static make() {
return new QMC1Crypto();
} }
} }

View File

@ -3,11 +3,14 @@ import type { CryptoBase } from '../CryptoBase';
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts'; import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts';
export class QMC2Crypto implements CryptoBase { export class QMC2Crypto implements CryptoBase {
cryptoName = 'QMCv2'; cryptoName = 'QMC/v2';
checkByDecryptHeader = false; checkByDecryptHeader = false;
decryptTargetAudio = true;
async decrypt(_buffer: ArrayBuffer, blob: Blob): Promise<Blob> { async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(blob, (p) => p.make.QMCv2(p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2))); return transformBlob(buffer, (p) => p.make.QMCv2(p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2)));
}
public static make() {
return new QMC2Crypto();
} }
} }

View File

@ -26,7 +26,6 @@ const u8Sub = (a: number, b: number) => {
export class XiamiCrypto implements CryptoBase { export class XiamiCrypto implements CryptoBase {
cryptoName = 'Xiami'; cryptoName = 'Xiami';
checkByDecryptHeader = false; checkByDecryptHeader = false;
decryptTargetAudio = true;
async checkBySignature(buffer: ArrayBuffer): Promise<boolean> { async checkBySignature(buffer: ArrayBuffer): Promise<boolean> {
const header = new DataView(buffer); const header = new DataView(buffer);
@ -45,4 +44,8 @@ export class XiamiCrypto implements CryptoBase {
} }
return decrypted; return decrypted;
} }
public static make() {
return new XiamiCrypto();
}
} }

View File

@ -0,0 +1,17 @@
export interface XimalayaAndroidKey {
contentKey: string;
init: number;
step: number;
}
export const XimalayaX2MKey: XimalayaAndroidKey = {
contentKey: 'xmly',
init: 0.615243,
step: 3.837465,
};
export const XimalayaX3MKey: XimalayaAndroidKey = {
contentKey: '3989d111aad5613940f4fc44b639b292',
init: 0.726354,
step: 3.948576,
};

View File

@ -0,0 +1,29 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase.js';
import { XimalayaAndroidKey, XimalayaX2MKey, XimalayaX3MKey } from './xmly_android.key.js';
export class XimalayaAndroidCrypto implements CryptoBase {
cryptoName = 'Ximalaya/Android';
checkByDecryptHeader = true;
constructor(private key: XimalayaAndroidKey) {}
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
const { contentKey, init, step } = this.key;
return transformBlob(buffer, (p) => {
const transformer = p.make.XimalayaAndroid(init, step, contentKey);
if (!transformer) {
throw new Error('could not make xmly transformer, is key invalid?');
}
return transformer;
});
}
public static makeX2M() {
return new XimalayaAndroidCrypto(XimalayaX2MKey);
}
public static makeX3M() {
return new XimalayaAndroidCrypto(XimalayaX3MKey);
}
}

View File

@ -1,7 +1,8 @@
import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@jixun/libparakeet'; import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@jixun/libparakeet';
import { toArrayBuffer } from './buffer';
export async function transformBlob( export async function transformBlob(
blob: Blob, blob: Blob | ArrayBuffer,
transformerFactory: (p: Parakeet) => Transformer | Promise<Transformer>, transformerFactory: (p: Parakeet) => Transformer | Promise<Transformer>,
parakeet?: Parakeet parakeet?: Parakeet
) { ) {
@ -12,7 +13,7 @@ export async function transformBlob(
const transformer = await transformerFactory(mod); const transformer = await transformerFactory(mod);
cleanup.push(() => transformer.delete()); cleanup.push(() => transformer.delete());
const reader = mod.make.Reader(await blob.arrayBuffer()); const reader = mod.make.Reader(await toArrayBuffer(blob));
cleanup.push(() => reader.delete()); cleanup.push(() => reader.delete());
const sink = mod.make.WriterSink(); const sink = mod.make.WriterSink();
@ -21,7 +22,7 @@ export async function transformBlob(
const result = transformer.Transform(writer, reader); const result = transformer.Transform(writer, reader);
if (result !== TransformResult.OK) { if (result !== TransformResult.OK) {
throw new Error(`transform failed with error: ${TransformResult[result]} (${result})`); throw new Error(`transformer<${transformer.Name}> failed with error: ${TransformResult[result]} (${result})`);
} }
return sink.collectBlob(); return sink.collectBlob();

View File

@ -46,17 +46,24 @@ class DecryptCommandHandler {
return null; return null;
} }
const decrypted = await this.log('decrypt', async () => crypto.decrypt(this.buffer, this.blob)); const decrypted = await this.log(`decrypt (${crypto.cryptoName})`, () => crypto.decrypt(this.buffer, this.blob));
// Check if we had a successful decryption // Check if we had a successful decryption
const audioExt = await this.log(`detect-audio-ext`, async () => { const audioExt = crypto.overrideExtension ?? (await this.detectAudioExtension(decrypted));
const header = await toArrayBuffer(decrypted.slice(0, TEST_FILE_HEADER_LEN)); if (crypto.checkByDecryptHeader && audioExt === 'bin') {
return this.parakeet.detectAudioExtension(header); return null;
}); }
return { decrypted: URL.createObjectURL(toBlob(decrypted)), ext: audioExt }; return { decrypted: URL.createObjectURL(toBlob(decrypted)), ext: audioExt };
} }
async detectAudioExtension(data: Blob | ArrayBuffer): Promise<string> {
return this.log(`detect-audio-ext`, async () => {
const header = await toArrayBuffer(data.slice(0, TEST_FILE_HEADER_LEN));
return this.parakeet.detectAudioExtension(header);
});
}
async acceptByDecryptFileHeader(crypto: CryptoBase): Promise<boolean> { async acceptByDecryptFileHeader(crypto: CryptoBase): Promise<boolean> {
// File too small, ignore. // File too small, ignore.
if (this.buffer.byteLength <= TEST_FILE_HEADER_LEN) { if (this.buffer.byteLength <= TEST_FILE_HEADER_LEN) {