exp: use @unlock-music/crypto backend for NCM decryption

This commit is contained in:
鲁树人 2024-09-14 20:32:31 +01:00
parent 22528481d5
commit 985620d188
8 changed files with 38 additions and 10 deletions

1
.npmrc
View File

@ -2,3 +2,4 @@ use-node-version=20.10.0
node-version=20.10.0 node-version=20.10.0
engine-strict=true engine-strict=true
@um:registry=https://git.unlock-music.dev/api/packages/um/npm/ @um:registry=https://git.unlock-music.dev/api/packages/um/npm/
@unlock-music:registry=https://git.unlock-music.dev/api/packages/um/npm/

View File

@ -24,6 +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.6",
"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

@ -41,6 +41,9 @@ importers:
'@um/libparakeet': '@um/libparakeet':
specifier: 0.4.5 specifier: 0.4.5
version: 0.4.5 version: 0.4.5
'@unlock-music/crypto':
specifier: 0.0.0-alpha.6
version: 0.0.0-alpha.6
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)
@ -1924,6 +1927,9 @@ 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.6':
resolution: {integrity: sha512-hv1oTXPzsNmqrP5dmjkLa4rfZtd4U/Cu1Bake71QEZhbY1WCbgEX4haCAcXXd6DMGNg/Hv1JXL2TcXNUaqIiFA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.6/crypto-0.0.0-alpha.6.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==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@ -6112,6 +6118,8 @@ snapshots:
'@ungap/structured-clone@1.2.0': {} '@ungap/structured-clone@1.2.0': {}
'@unlock-music/crypto@0.0.0-alpha.6': {}
'@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:
'@babel/core': 7.23.6 '@babel/core': 7.23.6

View File

@ -1,2 +0,0 @@
export const NCM_KEY = 'hzHRAmso5kInbaxW';
export const NCM_MAGIC_HEADER = new Uint8Array([0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d]);

View File

@ -1,18 +1,27 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase'; import type { CryptoBase } from '../CryptoBase';
import { NCM_KEY, NCM_MAGIC_HEADER } from './ncm_pc.key'; import { NCMFile } from '@unlock-music/crypto';
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
export class NCMCrypto implements CryptoBase { export class NCMCrypto implements CryptoBase {
cryptoName = 'NCM/PC'; cryptoName = 'NCM/PC';
checkByDecryptHeader = false; checkByDecryptHeader = false;
ncm = new NCMFile();
async checkBySignature(buffer: ArrayBuffer) { async checkBySignature(buffer: ArrayBuffer) {
const view = new DataView(buffer, 0, NCM_MAGIC_HEADER.byteLength); try {
return NCM_MAGIC_HEADER.every((value, i) => value === view.getUint8(i)); this.ncm.open(new Uint8Array(buffer));
} catch (error) {
return false;
}
return true;
} }
async decrypt(buffer: ArrayBuffer): Promise<Blob> { async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) => p.make.NeteaseNCM(NCM_KEY)); const audioBuffer = new Uint8Array(buffer.slice(this.ncm.audioOffset));
for (const [block, offset] of chunkBuffer(audioBuffer)) {
this.ncm.decrypt(block, offset);
}
return new Blob([audioBuffer]);
} }
public static make() { public static make() {

View File

@ -1,2 +1,11 @@
export const toArrayBuffer = async (src: Blob | ArrayBuffer) => (src instanceof Blob ? await src.arrayBuffer() : src); export const toArrayBuffer = async (src: Blob | ArrayBuffer) => (src instanceof Blob ? await src.arrayBuffer() : src);
export const toBlob = (src: Blob | ArrayBuffer) => (src instanceof Blob ? src : new Blob([src])); export const toBlob = (src: Blob | ArrayBuffer) => (src instanceof Blob ? src : new Blob([src]));
export function* chunkBuffer(buffer: Uint8Array, blockLen = 4096): Generator<[Uint8Array, number], void> {
const len = buffer.byteLength;
for (let i = 0; i < len; i += blockLen) {
const idxEnd = Math.min(i + blockLen, len);
const slice = buffer.subarray(i, idxEnd);
yield [slice, i];
}
}

View File

@ -5,6 +5,7 @@ import { allCryptoFactories } from '../../crypto/CryptoFactory';
import { toArrayBuffer, toBlob } from '~/decrypt-worker/util/buffer'; import { toArrayBuffer, toBlob } from '~/decrypt-worker/util/buffer';
import { CryptoBase, CryptoFactory } from '~/decrypt-worker/crypto/CryptoBase'; import { CryptoBase, CryptoFactory } from '~/decrypt-worker/crypto/CryptoBase';
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError'; import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError';
import { ready as umCryptoReady } from '@unlock-music/crypto';
// Use first 4MiB of the file to perform check. // Use first 4MiB of the file to perform check.
const TEST_FILE_HEADER_LEN = 4 * 1024 * 1024; const TEST_FILE_HEADER_LEN = 4 * 1024 * 1024;
@ -30,7 +31,7 @@ class DecryptCommandHandler {
const decryptor = factory(); const decryptor = factory();
try { try {
const result = await this.decryptFile(decryptor); const result = await this.tryDecryptFile(decryptor);
if (result === null) { if (result === null) {
continue; continue;
} }
@ -47,7 +48,7 @@ class DecryptCommandHandler {
throw new UnsupportedSourceFile('could not decrypt file: no working decryptor found'); throw new UnsupportedSourceFile('could not decrypt file: no working decryptor found');
} }
async decryptFile(crypto: CryptoBase) { async tryDecryptFile(crypto: CryptoBase) {
if (crypto.checkBySignature && !(await crypto.checkBySignature(this.buffer, this.options))) { if (crypto.checkBySignature && !(await crypto.checkBySignature(this.buffer, this.options))) {
return null; return null;
} }
@ -95,6 +96,7 @@ class DecryptCommandHandler {
export const workerDecryptHandler = async ({ id, blobURI, options }: DecryptCommandPayload) => { export const workerDecryptHandler = async ({ id, blobURI, options }: DecryptCommandPayload) => {
const label = `decrypt(${id})`; const label = `decrypt(${id})`;
return withTimeGroupedLogs(label, async () => { return withTimeGroupedLogs(label, async () => {
await umCryptoReady;
const parakeet = await timedLogger(`${label}/init`, fetchParakeet); const parakeet = await timedLogger(`${label}/init`, fetchParakeet);
const blob = await timedLogger(`${label}/fetch-src`, async () => fetch(blobURI).then((r) => r.blob())); const blob = await timedLogger(`${label}/fetch-src`, async () => fetch(blobURI).then((r) => r.blob()));
const buffer = await timedLogger(`${label}/read-src`, async () => blob.arrayBuffer()); const buffer = await timedLogger(`${label}/read-src`, async () => blob.arrayBuffer());

View File

@ -40,7 +40,7 @@ export default defineConfig({
}, },
base: './', base: './',
optimizeDeps: { optimizeDeps: {
exclude: ['@um/libparakeet', 'sql.js'], exclude: ['@um/libparakeet', '@unlock-music/crypto', 'sql.js'],
}, },
plugins: [ plugins: [
replace({ replace({