refactor: improve flow of decryption (#2)

This commit is contained in:
鲁树人 2023-05-21 23:38:32 +01:00
parent e6bc60b9af
commit fa2629ae6c
7 changed files with 43 additions and 21 deletions

View File

@ -1,7 +1,12 @@
export interface CryptoBase {
cryptoName: string;
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>;
decrypt(buffer: ArrayBuffer, blob: Blob): Promise<Blob | ArrayBuffer>;

View File

@ -6,14 +6,14 @@ import { XiamiCrypto } from './xiami/xiami';
export const allCryptoFactories: CryptoFactory[] = [
// Xiami (*.xm)
() => new XiamiCrypto(),
XiamiCrypto.make,
// QMCv2 (*.mflac)
() => new QMC2Crypto(),
QMC2Crypto.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.
// QMCv1 (*.qmcflac)
() => new QMC1Crypto(),
QMC1Crypto.make,
];

View File

@ -3,11 +3,14 @@ import type { CryptoBase } from '../CryptoBase';
import key from './qmc_v1.key.ts';
export class QMC1Crypto implements CryptoBase {
cryptoName = 'QMCv1';
cryptoName = 'QMC/v1';
checkByDecryptHeader = true;
decryptTargetAudio = true;
async decrypt(_buffer: ArrayBuffer, blob: Blob): Promise<Blob> {
return transformBlob(blob, (p) => p.make.QMCv1(key));
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
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';
export class QMC2Crypto implements CryptoBase {
cryptoName = 'QMCv2';
cryptoName = 'QMC/v2';
checkByDecryptHeader = false;
decryptTargetAudio = true;
async decrypt(_buffer: ArrayBuffer, blob: Blob): Promise<Blob> {
return transformBlob(blob, (p) => p.make.QMCv2(p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2)));
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
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 {
cryptoName = 'Xiami';
checkByDecryptHeader = false;
decryptTargetAudio = true;
async checkBySignature(buffer: ArrayBuffer): Promise<boolean> {
const header = new DataView(buffer);
@ -45,4 +44,8 @@ export class XiamiCrypto implements CryptoBase {
}
return decrypted;
}
public static make() {
return new XiamiCrypto();
}
}

View File

@ -1,7 +1,8 @@
import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@jixun/libparakeet';
import { toArrayBuffer } from './buffer';
export async function transformBlob(
blob: Blob,
blob: Blob | ArrayBuffer,
transformerFactory: (p: Parakeet) => Transformer | Promise<Transformer>,
parakeet?: Parakeet
) {
@ -12,7 +13,7 @@ export async function transformBlob(
const transformer = await transformerFactory(mod);
cleanup.push(() => transformer.delete());
const reader = mod.make.Reader(await blob.arrayBuffer());
const reader = mod.make.Reader(await toArrayBuffer(blob));
cleanup.push(() => reader.delete());
const sink = mod.make.WriterSink();
@ -21,7 +22,7 @@ export async function transformBlob(
const result = transformer.Transform(writer, reader);
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();

View File

@ -46,17 +46,24 @@ class DecryptCommandHandler {
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
const audioExt = await this.log(`detect-audio-ext`, async () => {
const header = await toArrayBuffer(decrypted.slice(0, TEST_FILE_HEADER_LEN));
return this.parakeet.detectAudioExtension(header);
});
const audioExt = crypto.overrideExtension ?? (await this.detectAudioExtension(decrypted));
if (crypto.checkByDecryptHeader && audioExt === 'bin') {
return null;
}
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> {
// File too small, ignore.
if (this.buffer.byteLength <= TEST_FILE_HEADER_LEN) {