Compare commits
3 Commits
0b2000ebe2
...
c3809b48f7
Author | SHA1 | Date | |
---|---|---|---|
c3809b48f7 | |||
fa2629ae6c | |||
e6bc60b9af |
@ -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",
|
||||||
|
@ -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:
|
||||||
|
@ -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>;
|
||||||
|
@ -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,
|
||||||
];
|
];
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
17
src/decrypt-worker/crypto/xmly/xmly_android.key.ts
Normal file
17
src/decrypt-worker/crypto/xmly/xmly_android.key.ts
Normal 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,
|
||||||
|
};
|
29
src/decrypt-worker/crypto/xmly/xmly_android.ts
Normal file
29
src/decrypt-worker/crypto/xmly/xmly_android.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user