From 985620d18887255834244107f7308bfd1a2bcd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sat, 14 Sep 2024 20:32:31 +0100 Subject: [PATCH] exp: use `@unlock-music/crypto` backend for NCM decryption --- .npmrc | 1 + package.json | 1 + pnpm-lock.yaml | 8 ++++++++ src/decrypt-worker/crypto/ncm/ncm_pc.key.ts | 2 -- src/decrypt-worker/crypto/ncm/ncm_pc.ts | 19 ++++++++++++++----- src/decrypt-worker/util/buffer.ts | 9 +++++++++ src/decrypt-worker/worker/handler/decrypt.ts | 6 ++++-- vite.config.ts | 2 +- 8 files changed, 38 insertions(+), 10 deletions(-) delete mode 100644 src/decrypt-worker/crypto/ncm/ncm_pc.key.ts diff --git a/.npmrc b/.npmrc index 03a62a7..4a2dd39 100644 --- a/.npmrc +++ b/.npmrc @@ -2,3 +2,4 @@ use-node-version=20.10.0 node-version=20.10.0 engine-strict=true @um:registry=https://git.unlock-music.dev/api/packages/um/npm/ +@unlock-music:registry=https://git.unlock-music.dev/api/packages/um/npm/ diff --git a/package.json b/package.json index 59d738e..03278bc 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", "@um/libparakeet": "0.4.5", + "@unlock-music/crypto": "0.0.0-alpha.6", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2de559..84a7bf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@um/libparakeet': specifier: 0.4.5 version: 0.4.5 + '@unlock-music/crypto': + specifier: 0.0.0-alpha.6 + version: 0.0.0-alpha.6 framer-motion: specifier: ^10.16.16 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': 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': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6112,6 +6118,8 @@ snapshots: '@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))': dependencies: '@babel/core': 7.23.6 diff --git a/src/decrypt-worker/crypto/ncm/ncm_pc.key.ts b/src/decrypt-worker/crypto/ncm/ncm_pc.key.ts deleted file mode 100644 index 173e640..0000000 --- a/src/decrypt-worker/crypto/ncm/ncm_pc.key.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const NCM_KEY = 'hzHRAmso5kInbaxW'; -export const NCM_MAGIC_HEADER = new Uint8Array([0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d]); diff --git a/src/decrypt-worker/crypto/ncm/ncm_pc.ts b/src/decrypt-worker/crypto/ncm/ncm_pc.ts index 15e094b..5c847ec 100644 --- a/src/decrypt-worker/crypto/ncm/ncm_pc.ts +++ b/src/decrypt-worker/crypto/ncm/ncm_pc.ts @@ -1,18 +1,27 @@ -import { transformBlob } from '~/decrypt-worker/util/transformBlob'; 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 { cryptoName = 'NCM/PC'; checkByDecryptHeader = false; + ncm = new NCMFile(); async checkBySignature(buffer: ArrayBuffer) { - const view = new DataView(buffer, 0, NCM_MAGIC_HEADER.byteLength); - return NCM_MAGIC_HEADER.every((value, i) => value === view.getUint8(i)); + try { + this.ncm.open(new Uint8Array(buffer)); + } catch (error) { + return false; + } + return true; } async decrypt(buffer: ArrayBuffer): Promise { - 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() { diff --git a/src/decrypt-worker/util/buffer.ts b/src/decrypt-worker/util/buffer.ts index a150c28..c5be9de 100644 --- a/src/decrypt-worker/util/buffer.ts +++ b/src/decrypt-worker/util/buffer.ts @@ -1,2 +1,11 @@ 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 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]; + } +} diff --git a/src/decrypt-worker/worker/handler/decrypt.ts b/src/decrypt-worker/worker/handler/decrypt.ts index 0307b4c..5ff600e 100644 --- a/src/decrypt-worker/worker/handler/decrypt.ts +++ b/src/decrypt-worker/worker/handler/decrypt.ts @@ -5,6 +5,7 @@ import { allCryptoFactories } from '../../crypto/CryptoFactory'; import { toArrayBuffer, toBlob } from '~/decrypt-worker/util/buffer'; import { CryptoBase, CryptoFactory } from '~/decrypt-worker/crypto/CryptoBase'; import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError'; +import { ready as umCryptoReady } from '@unlock-music/crypto'; // Use first 4MiB of the file to perform check. const TEST_FILE_HEADER_LEN = 4 * 1024 * 1024; @@ -30,7 +31,7 @@ class DecryptCommandHandler { const decryptor = factory(); try { - const result = await this.decryptFile(decryptor); + const result = await this.tryDecryptFile(decryptor); if (result === null) { continue; } @@ -47,7 +48,7 @@ class DecryptCommandHandler { 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))) { return null; } @@ -95,6 +96,7 @@ class DecryptCommandHandler { export const workerDecryptHandler = async ({ id, blobURI, options }: DecryptCommandPayload) => { const label = `decrypt(${id})`; return withTimeGroupedLogs(label, async () => { + await umCryptoReady; const parakeet = await timedLogger(`${label}/init`, fetchParakeet); const blob = await timedLogger(`${label}/fetch-src`, async () => fetch(blobURI).then((r) => r.blob())); const buffer = await timedLogger(`${label}/read-src`, async () => blob.arrayBuffer()); diff --git a/vite.config.ts b/vite.config.ts index e231301..f90db5b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,7 +40,7 @@ export default defineConfig({ }, base: './', optimizeDeps: { - exclude: ['@um/libparakeet', 'sql.js'], + exclude: ['@um/libparakeet', '@unlock-music/crypto', 'sql.js'], }, plugins: [ replace({