mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 11:33:02 +00:00
Dependency upgrade + lib_um_crypto_rust (#78)
Co-authored-by: 鲁树人 <lu.shuren@um-react.app> Co-committed-by: 鲁树人 <lu.shuren@um-react.app>
This commit is contained in:
33
src/decrypt-worker/decipher/KugouMusic.ts
Normal file
33
src/decrypt-worker/decipher/KugouMusic.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
||||
import { KuGou } from '@unlock-music/crypto';
|
||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||
|
||||
export class KugouMusicDecipher implements DecipherInstance {
|
||||
cipherName = 'Kugou';
|
||||
|
||||
async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||
let kgm: KuGou | undefined;
|
||||
|
||||
try {
|
||||
kgm = KuGou.from_header(buffer.subarray(0, 0x400));
|
||||
|
||||
const audioBuffer = new Uint8Array(buffer.subarray(0x400));
|
||||
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||
kgm.decrypt(block, offset);
|
||||
}
|
||||
|
||||
return {
|
||||
status: Status.OK,
|
||||
cipherName: this.cipherName,
|
||||
data: audioBuffer,
|
||||
};
|
||||
} finally {
|
||||
kgm?.free();
|
||||
}
|
||||
}
|
||||
|
||||
public static make() {
|
||||
return new KugouMusicDecipher();
|
||||
}
|
||||
}
|
||||
35
src/decrypt-worker/decipher/KuwoMusic.ts
Normal file
35
src/decrypt-worker/decipher/KuwoMusic.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
||||
import { KuwoHeader, KWMDecipher } from '@unlock-music/crypto';
|
||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||
|
||||
export class KuwoMusicDecipher implements DecipherInstance {
|
||||
cipherName = 'Kuwo';
|
||||
|
||||
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||
let header: KuwoHeader | undefined;
|
||||
let kwm: KWMDecipher | undefined;
|
||||
|
||||
try {
|
||||
header = KuwoHeader.parse(buffer.subarray(0, 0x400));
|
||||
kwm = new KWMDecipher(header, options.kwm2key);
|
||||
|
||||
const audioBuffer = new Uint8Array(buffer.subarray(0x400));
|
||||
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||
kwm.decrypt(block, offset);
|
||||
}
|
||||
return {
|
||||
status: Status.OK,
|
||||
cipherName: this.cipherName,
|
||||
data: audioBuffer,
|
||||
};
|
||||
} finally {
|
||||
kwm?.free();
|
||||
header?.free();
|
||||
}
|
||||
}
|
||||
|
||||
public static make() {
|
||||
return new KuwoMusicDecipher();
|
||||
}
|
||||
}
|
||||
27
src/decrypt-worker/decipher/Migu3d.ts
Normal file
27
src/decrypt-worker/decipher/Migu3d.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts';
|
||||
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||
import { Migu3D } from '@unlock-music/crypto';
|
||||
|
||||
export class Migu3DKeylessDecipher implements DecipherInstance {
|
||||
cipherName = 'Migu3D (Keyless)';
|
||||
|
||||
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
|
||||
const mg3d = Migu3D.fromHeader(buffer.subarray(0, 0x100));
|
||||
const audioBuffer = new Uint8Array(buffer);
|
||||
|
||||
for (const [block, i] of chunkBuffer(audioBuffer)) {
|
||||
mg3d.decrypt(block, i);
|
||||
}
|
||||
mg3d.free();
|
||||
|
||||
return {
|
||||
cipherName: this.cipherName,
|
||||
status: Status.OK,
|
||||
data: audioBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
public static make() {
|
||||
return new Migu3DKeylessDecipher();
|
||||
}
|
||||
}
|
||||
42
src/decrypt-worker/decipher/NetEaseCloudMusic.ts
Normal file
42
src/decrypt-worker/decipher/NetEaseCloudMusic.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
||||
import { NCMFile } from '@unlock-music/crypto';
|
||||
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
|
||||
|
||||
export class NetEaseCloudMusicDecipher implements DecipherInstance {
|
||||
cipherName = 'NCM/PC';
|
||||
|
||||
tryInit(ncm: NCMFile, buffer: Uint8Array) {
|
||||
let neededLength = 1024;
|
||||
while (neededLength !== 0) {
|
||||
console.debug('NCM/open: read %d bytes', neededLength);
|
||||
neededLength = ncm.open(buffer.subarray(0, neededLength));
|
||||
if (neededLength === -1) {
|
||||
throw new UnsupportedSourceFile('file is not ncm');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
|
||||
const ncm = new NCMFile();
|
||||
try {
|
||||
this.tryInit(ncm, buffer);
|
||||
|
||||
const audioBuffer = buffer.slice(ncm.audioOffset);
|
||||
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||
ncm.decrypt(block, offset);
|
||||
}
|
||||
return {
|
||||
status: Status.OK,
|
||||
cipherName: this.cipherName,
|
||||
data: audioBuffer,
|
||||
};
|
||||
} finally {
|
||||
ncm.free();
|
||||
}
|
||||
}
|
||||
|
||||
public static make() {
|
||||
return new NetEaseCloudMusicDecipher();
|
||||
}
|
||||
}
|
||||
74
src/decrypt-worker/decipher/QQMusic.ts
Normal file
74
src/decrypt-worker/decipher/QQMusic.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
|
||||
import { decryptQMC1, QMC2, QMCFooter } from '@unlock-music/crypto';
|
||||
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
|
||||
import { isDataLooksLikeAudio } from '~/decrypt-worker/util/audioType.ts';
|
||||
|
||||
export class QQMusicV1Decipher implements DecipherInstance {
|
||||
cipherName = 'QQMusic/QMC1';
|
||||
|
||||
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
|
||||
const header = buffer.slice(0, 0x20);
|
||||
decryptQMC1(header, 0);
|
||||
if (!isDataLooksLikeAudio(header)) {
|
||||
throw new UnsupportedSourceFile('does not look like QMC file');
|
||||
}
|
||||
|
||||
const audioBuffer = new Uint8Array(buffer);
|
||||
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||
decryptQMC1(block, offset);
|
||||
}
|
||||
return {
|
||||
status: Status.OK,
|
||||
cipherName: this.cipherName,
|
||||
data: audioBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
public static create() {
|
||||
return new QQMusicV1Decipher();
|
||||
}
|
||||
}
|
||||
|
||||
export class QQMusicV2Decipher implements DecipherInstance {
|
||||
cipherName: string;
|
||||
|
||||
constructor(private readonly useUserKey: boolean) {
|
||||
this.cipherName = `QQMusic/QMC2(user_key=${+useUserKey})`;
|
||||
}
|
||||
|
||||
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||
const footer = QMCFooter.parse(buffer.subarray(buffer.byteLength - 1024));
|
||||
if (!footer) {
|
||||
throw new UnsupportedSourceFile('Not QMC2 File');
|
||||
}
|
||||
|
||||
const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size);
|
||||
const ekey = this.useUserKey ? options.qmc2Key : footer.ekey;
|
||||
footer.free();
|
||||
if (!ekey) {
|
||||
throw new Error('EKey missing');
|
||||
}
|
||||
|
||||
const qmc2 = new QMC2(ekey);
|
||||
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||
qmc2.decrypt(block, offset);
|
||||
}
|
||||
qmc2.free();
|
||||
|
||||
return {
|
||||
status: Status.OK,
|
||||
cipherName: this.cipherName,
|
||||
data: audioBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
public static createWithUserKey() {
|
||||
return new QQMusicV2Decipher(true);
|
||||
}
|
||||
|
||||
public static createWithEmbeddedEKey() {
|
||||
return new QQMusicV2Decipher(false);
|
||||
}
|
||||
}
|
||||
37
src/decrypt-worker/decipher/QingTingFM.ts
Normal file
37
src/decrypt-worker/decipher/QingTingFM.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts';
|
||||
import { QingTingFM } from '@unlock-music/crypto';
|
||||
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
|
||||
import { unhex } from '~/util/hex.ts';
|
||||
|
||||
export class QignTingFMDecipher implements DecipherInstance {
|
||||
cipherName = 'QingTingFM (Android, qta)';
|
||||
|
||||
async decrypt(buffer: Uint8Array, opts: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||
const key = unhex(opts.qingTingAndroidKey || '');
|
||||
const iv = QingTingFM.getFileIV(opts.fileName);
|
||||
|
||||
if (key.byteLength !== 16 || iv.byteLength !== 16) {
|
||||
return {
|
||||
status: Status.FAILED,
|
||||
message: 'device key or iv invalid',
|
||||
};
|
||||
}
|
||||
|
||||
const qtfm = new QingTingFM(key, iv);
|
||||
const audioBuffer = new Uint8Array(buffer);
|
||||
for (const [block, i] of chunkBuffer(audioBuffer)) {
|
||||
qtfm.decrypt(block, i);
|
||||
}
|
||||
|
||||
return {
|
||||
cipherName: this.cipherName,
|
||||
status: Status.OK,
|
||||
data: audioBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
public static make() {
|
||||
return new QignTingFMDecipher();
|
||||
}
|
||||
}
|
||||
18
src/decrypt-worker/decipher/Transparent.ts
Normal file
18
src/decrypt-worker/decipher/Transparent.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts';
|
||||
|
||||
export class TransparentDecipher implements DecipherInstance {
|
||||
cipherName = 'none';
|
||||
|
||||
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
|
||||
return {
|
||||
cipherName: 'None',
|
||||
status: Status.OK,
|
||||
data: buffer,
|
||||
message: 'No decipher applied',
|
||||
};
|
||||
}
|
||||
|
||||
public static make() {
|
||||
return new TransparentDecipher();
|
||||
}
|
||||
}
|
||||
28
src/decrypt-worker/decipher/XiamiMusic.ts
Normal file
28
src/decrypt-worker/decipher/XiamiMusic.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
71
src/decrypt-worker/decipher/Ximalaya.ts
Normal file
71
src/decrypt-worker/decipher/Ximalaya.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user