From e8b220b3df8f122df8b6fd0e6b09abd32225c0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Wed, 18 Sep 2024 23:02:05 +0100 Subject: [PATCH] feat: add xmly support --- src/decrypt-worker/Deciphers.ts | 8 ++- src/decrypt-worker/decipher/Ximalaya.ts | 70 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/decrypt-worker/decipher/Ximalaya.ts diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index ace5f1c..a98b6ac 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -4,6 +4,7 @@ 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'; import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; +import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts'; export enum Status { OK = 0, @@ -46,6 +47,9 @@ export const allCryptoFactories: DecipherFactory[] = [ // KWMv1 (*.kwm) KuwoMusicDecipher.make, + // Ximalaya PC (*.xm) + XimalayaPCDecipher.make, + // Xiami (*.xm) // XiamiCrypto.make, @@ -67,8 +71,8 @@ export const allCryptoFactories: DecipherFactory[] = [ QQMusicV1Decipher.create, // Ximalaya (Android) - // XimalayaAndroidCrypto.makeX2M, - // XimalayaAndroidCrypto.makeX3M, + XimalayaAndroidDecipher.makeX2M, + XimalayaAndroidDecipher.makeX3M, // QingTingFM (Android) // QingTingFM$Device.make, diff --git a/src/decrypt-worker/decipher/Ximalaya.ts b/src/decrypt-worker/decipher/Ximalaya.ts new file mode 100644 index 0000000..fae3734 --- /dev/null +++ b/src/decrypt-worker/decipher/Ximalaya.ts @@ -0,0 +1,70 @@ +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 { + // 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 { + // 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; + + 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(); + } +}