Compare commits

...

5 Commits

6 changed files with 116 additions and 15 deletions

View File

@ -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.12", "@unlock-music/crypto": "0.0.0-alpha.13",
"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",

View File

@ -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.12 specifier: 0.0.0-alpha.13
version: 0.0.0-alpha.12 version: 0.0.0-alpha.13
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.12': '@unlock-music/crypto@0.0.0-alpha.13':
resolution: {integrity: sha512-Q24cq653CmD8sj/D1M6wHYtXJIX3YIgnvbPtO+aHnY07J0ZXvkqNh+6a3hBrGGLYzcSWioAw2xxf2rFEQ3q35A==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.12/crypto-0.0.0-alpha.12.tgz} resolution: {integrity: sha512-4afez9SjfY5EN17JsifXCc50dGDIImBGXnoxQbBQkB4TguJt2ePWCDS8UbnBhnfeAQUypWBC6qicOQSwVM36hw==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.13/crypto-0.0.0-alpha.13.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.12': {} '@unlock-music/crypto@0.0.0-alpha.13': {}
'@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:

View File

@ -4,6 +4,8 @@ import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts'; import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts';
import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts';
import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts';
import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts';
import { XiamiDecipher } from '~/decrypt-worker/decipher/XiamiMusic.ts';
export enum Status { export enum Status {
OK = 0, OK = 0,
@ -46,8 +48,11 @@ export const allCryptoFactories: DecipherFactory[] = [
// KWMv1 (*.kwm) // KWMv1 (*.kwm)
KuwoMusicDecipher.make, KuwoMusicDecipher.make,
// Ximalaya PC (*.xm)
XimalayaPCDecipher.make,
// Xiami (*.xm) // Xiami (*.xm)
// XiamiCrypto.make, XiamiDecipher.make,
/// File with a fixed footer goes second /// File with a fixed footer goes second
@ -67,8 +72,8 @@ export const allCryptoFactories: DecipherFactory[] = [
QQMusicV1Decipher.create, QQMusicV1Decipher.create,
// Ximalaya (Android) // Ximalaya (Android)
// XimalayaAndroidCrypto.makeX2M, XimalayaAndroidDecipher.makeX2M,
// XimalayaAndroidCrypto.makeX3M, XimalayaAndroidDecipher.makeX3M,
// QingTingFM (Android) // QingTingFM (Android)
// QingTingFM$Device.make, // QingTingFM$Device.make,

View File

@ -1,5 +1,5 @@
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
import { KuGouDecipher, KuGouHeader } from '@unlock-music/crypto'; import { KuGou } from '@unlock-music/crypto';
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
@ -7,12 +7,10 @@ export class KugouMusicDecipher implements DecipherInstance {
cipherName = 'Kugou'; cipherName = 'Kugou';
async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> { async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
let kgm: KuGouDecipher | undefined; let kgm: KuGou | undefined;
let header: KuGouHeader | undefined;
try { try {
header = KuGouHeader.parse(buffer.subarray(0, 0x400)); kgm = KuGou.from_header(buffer.subarray(0, 0x400));
kgm = new KuGouDecipher(header);
const audioBuffer = new Uint8Array(buffer.subarray(0x400)); const audioBuffer = new Uint8Array(buffer.subarray(0x400));
for (const [block, offset] of chunkBuffer(audioBuffer)) { for (const [block, offset] of chunkBuffer(audioBuffer)) {
@ -26,7 +24,6 @@ export class KugouMusicDecipher implements DecipherInstance {
}; };
} finally { } finally {
kgm?.free(); kgm?.free();
header?.free();
} }
} }

View File

@ -0,0 +1,28 @@
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts';
import { Xiami } from '@unlock-music/crypto';
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
export class XiamiDecipher implements DecipherInstance {
cipherName = 'Xiami (XM)';
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
const xm = Xiami.from_header(buffer.subarray(0, 0x10));
const { copyPlainLength } = xm;
const audioBuffer = buffer.slice(0x10);
for (const [block] of chunkBuffer(audioBuffer.subarray(copyPlainLength))) {
xm.decrypt(block);
}
xm.free();
return {
cipherName: this.cipherName,
status: Status.OK,
data: audioBuffer,
};
}
public static make() {
return new XiamiDecipher();
}
}

View File

@ -0,0 +1,71 @@
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { decryptX2MHeader, decryptX3MHeader, XmlyPC } from '@unlock-music/crypto';
import { isDataLooksLikeAudio } from '~/decrypt-worker/util/audioType.ts';
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
export class XimalayaAndroidDecipher implements DecipherInstance {
cipherName: string;
constructor(
private decipher: (buffer: Uint8Array) => void,
private cipherType: string,
) {
this.cipherName = `Ximalaya (Android, ${cipherType})`;
}
async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
// Detect with first 0x400 bytes
const slice = buffer.slice(0, 0x400);
this.decipher(slice);
if (!isDataLooksLikeAudio(slice)) {
throw new UnsupportedSourceFile(`Not a Xmly android file (${this.cipherType})`);
}
const result = new Uint8Array(buffer);
result.set(slice, 0);
return {
cipherName: this.cipherName,
status: Status.OK,
data: result,
};
}
public static makeX2M() {
return new XimalayaAndroidDecipher(decryptX2MHeader, 'X2M');
}
public static makeX3M() {
return new XimalayaAndroidDecipher(decryptX3MHeader, 'X3M');
}
}
export class XimalayaPCDecipher implements DecipherInstance {
cipherName = 'Ximalaya (PC)';
async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
// Detect with first 0x400 bytes
const headerSize = XmlyPC.getHeaderSize(buffer.subarray(0, 1024));
const xm = new XmlyPC(buffer.subarray(0, headerSize));
const { audioHeader, encryptedHeaderOffset, encryptedHeaderSize } = xm;
const plainAudioDataOffset = encryptedHeaderOffset + encryptedHeaderSize;
const plainAudioDataLength = buffer.byteLength - plainAudioDataOffset;
const encryptedAudioPart = buffer.slice(encryptedHeaderOffset, plainAudioDataOffset);
const encryptedAudioPartLen = xm.decrypt(encryptedAudioPart);
const audioSize = audioHeader.byteLength + encryptedAudioPartLen + plainAudioDataLength;
xm.free();
const result = new Uint8Array(audioSize);
result.set(audioHeader);
result.set(encryptedAudioPart, audioHeader.byteLength);
result.set(buffer.subarray(plainAudioDataOffset), audioHeader.byteLength + encryptedAudioPartLen);
return {
status: Status.OK,
data: result,
cipherName: this.cipherName,
};
}
public static make() {
return new XimalayaPCDecipher();
}
}