From 85fb68cd66dbb474091a2c392f424011d50918c6 Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Wed, 8 Nov 2023 20:40:41 +0000 Subject: [PATCH 1/2] feat: add support for kuwo ios ekey db --- src/crypto/parseKuwo.ts | 4 +- src/features/settings/keyFormats.ts | 17 ++++-- .../settings/panels/KWMv2/InstructionsIOS.tsx | 33 +++++++++++ .../panels/KWMv2/KWMv2AllInstructions.tsx | 5 ++ .../settings/panels/PanelKWMv2Key.tsx | 8 ++- src/theme.ts | 5 ++ src/util/MMKVParser.ts | 40 ++------------ src/util/__tests__/MMKVParser.test.ts | 52 ++++++------------ .../__tests__/__fixture__/qm_optional.mmkv | Bin 48 -> 0 bytes .../__tests__/__fixture__/kuwo_android.mmkv} | Bin .../mmkv/__tests__/__fixture__/kuwo_ios.mmkv | Bin 0 -> 64 bytes .../{ => mmkv}/__tests__/__fixture__/qm.mmkv | Bin 288 -> 288 bytes src/util/mmkv/__tests__/kuwo.test.ts | 31 +++++++++++ src/util/mmkv/__tests__/qm.test.ts | 15 +++++ src/util/mmkv/kuwo.ts | 41 ++++++++++++++ src/util/mmkv/qm.ts | 14 +++++ vite.config.ts | 1 - 17 files changed, 186 insertions(+), 80 deletions(-) create mode 100644 src/features/settings/panels/KWMv2/InstructionsIOS.tsx delete mode 100644 src/util/__tests__/__fixture__/qm_optional.mmkv rename src/util/{__tests__/__fixture__/kuwo.mmkv => mmkv/__tests__/__fixture__/kuwo_android.mmkv} (100%) create mode 100644 src/util/mmkv/__tests__/__fixture__/kuwo_ios.mmkv rename src/util/{ => mmkv}/__tests__/__fixture__/qm.mmkv (77%) create mode 100644 src/util/mmkv/__tests__/kuwo.test.ts create mode 100644 src/util/mmkv/__tests__/qm.test.ts create mode 100644 src/util/mmkv/kuwo.ts create mode 100644 src/util/mmkv/qm.ts diff --git a/src/crypto/parseKuwo.ts b/src/crypto/parseKuwo.ts index db6cf90..5de9b1d 100644 --- a/src/crypto/parseKuwo.ts +++ b/src/crypto/parseKuwo.ts @@ -7,9 +7,11 @@ export interface KuwoHeader { quality: string; } +const KUWO_MAGIC_HDRS = new Set(['yeelion-kuwo\x00\x00\x00\x00', 'yeelion-kuwo-tme']); + export function parseKuwoHeader(view: DataView): KuwoHeader | null { const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10); - if (bytesToUTF8String(magic) !== 'yeelion-kuwo-tme') { + if (!KUWO_MAGIC_HDRS.has(bytesToUTF8String(magic))) { return null; // not kuwo-encrypted file } diff --git a/src/features/settings/keyFormats.ts b/src/features/settings/keyFormats.ts index a9647d6..17df923 100644 --- a/src/features/settings/keyFormats.ts +++ b/src/features/settings/keyFormats.ts @@ -3,7 +3,7 @@ import { objectify } from 'radash'; export function productionKeyToStaging>( src: P, - make: (k: keyof P, v: P[keyof P]) => null | S + make: (k: keyof P, v: P[keyof P]) => null | S, ): S[] { const result: S[] = []; for (const [key, value] of Object.entries(src)) { @@ -31,7 +31,7 @@ export const qmc2StagingToProductionKey = (key: StagingQMCv2Key) => key.name.nor export const qmc2StagingToProductionValue = (key: StagingQMCv2Key) => key.ekey.trim(); export const qmc2ProductionToStaging = ( key: keyof ProductionQMCv2Keys, - value: ProductionQMCv2Keys[keyof ProductionQMCv2Keys] + value: ProductionQMCv2Keys[keyof ProductionQMCv2Keys], ): StagingQMCv2Key => { return { id: nanoid(), @@ -44,7 +44,13 @@ export const qmc2ProductionToStaging = ( export interface StagingKWMv2Key { id: string; + /** + * Resource ID + */ rid: string; + /** + * Quality String + */ quality: string; ekey: string; } @@ -58,16 +64,17 @@ export const parseKwm2ProductionKey = (key: string): null | { rid: string; quali return { rid, quality }; }; -export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality}`; +export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality.replace(/[\D]/g, '')}`; export const kwm2StagingToProductionValue = (key: StagingKWMv2Key) => key.ekey; export const kwm2ProductionToStaging = ( key: keyof ProductionKWMv2Keys, - value: ProductionKWMv2Keys[keyof ProductionKWMv2Keys] + value: ProductionKWMv2Keys[keyof ProductionKWMv2Keys], ): null | StagingKWMv2Key => { if (typeof value !== 'string') return null; const parsed = parseKwm2ProductionKey(key); if (!parsed) return null; + const { quality, rid } = parsed; - return { id: nanoid(), rid: parsed.rid, quality: parsed.quality, ekey: value }; + return { id: nanoid(), rid, quality, ekey: value }; }; diff --git a/src/features/settings/panels/KWMv2/InstructionsIOS.tsx b/src/features/settings/panels/KWMv2/InstructionsIOS.tsx new file mode 100644 index 0000000..5d79b6f --- /dev/null +++ b/src/features/settings/panels/KWMv2/InstructionsIOS.tsx @@ -0,0 +1,33 @@ +import { Code, ListItem, OrderedList, Text, chakra } from '@chakra-ui/react'; + +const KUWO_IOS_DIR = '/var/mobile/Containers/Data/Application/<酷我数据目录>/mmkv'; + +export function InstructionsIOS() { + return ( + <> + 你需要越狱来访问 iOS 应用的私有数据。 + + ⚠️ 请注意,越狱通常意味着你的设备 + 将失去保修资格。 + + + + + 访问设备的这个目录: + {KUWO_IOS_DIR} + + + + + 提取密钥数据库文件 kw_ekey 至浏览器可访问的目录,如下载目录。 + + + + + 提交刚刚提取的 kw_ekey 密钥数据库。 + + + + + ); +} diff --git a/src/features/settings/panels/KWMv2/KWMv2AllInstructions.tsx b/src/features/settings/panels/KWMv2/KWMv2AllInstructions.tsx index 144e604..36a4cf8 100644 --- a/src/features/settings/panels/KWMv2/KWMv2AllInstructions.tsx +++ b/src/features/settings/panels/KWMv2/KWMv2AllInstructions.tsx @@ -1,12 +1,14 @@ import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react'; import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction'; import { InstructionsPC } from './InstructionsPC'; +import { InstructionsIOS } from './InstructionsIOS'; export function KWMv2AllInstructions() { return ( <> 安卓 + iOS Windows @@ -16,6 +18,9 @@ export function KWMv2AllInstructions() { file="cn.kuwo.player.mmkv.defaultconfig" /> + + + diff --git a/src/features/settings/panels/PanelKWMv2Key.tsx b/src/features/settings/panels/PanelKWMv2Key.tsx index ca97311..b735fb3 100644 --- a/src/features/settings/panels/PanelKWMv2Key.tsx +++ b/src/features/settings/panels/PanelKWMv2Key.tsx @@ -22,7 +22,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md'; import { ImportSecretModal } from '~/components/ImportSecretModal'; -import { MMKVParser } from '~/util/MMKVParser'; +import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '~/util/mmkv/kuwo'; import { kwm2AddKey, kwm2ClearKeys, kwm2ImportKeys } from '../settingsSlice'; import { selectStagingKWMv2Keys } from '../settingsSelector'; @@ -41,9 +41,11 @@ export function PanelKWMv2Key() { const handleSecretImport = async (file: File) => { let keys: Omit[] | null = null; if (/cn\.kuwo\.player\.mmkv/i.test(file.name)) { - const fileBuffer = await file.arrayBuffer(); - keys = MMKVParser.parseKuwoEKey(new DataView(fileBuffer)); + keys = parseAndroidKuwoEKey(new DataView(await file.arrayBuffer())); + } else if (/kw_ekey/.test(file.name)) { + keys = parseIosKuwoEKey(new DataView(await file.arrayBuffer())); } + if (keys?.length === 0) { toast({ title: '未导入密钥', diff --git a/src/theme.ts b/src/theme.ts index ee8c96a..53ffc0c 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -9,6 +9,11 @@ export const theme = extendTheme({ 'Segoe UI,Helvetica,Arial,sans-serif', 'Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol', ].join(','), + mono: [ + 'SFMono-Regular,Menlo,Monaco', + '"Sarasa Mono CJK SC",', + 'Consolas,"Liberation Mono","Courier New",monospace', + ].join(','), }, components: { Button: { diff --git a/src/util/MMKVParser.ts b/src/util/MMKVParser.ts index 29b3ed5..bd9b2b6 100644 --- a/src/util/MMKVParser.ts +++ b/src/util/MMKVParser.ts @@ -1,4 +1,3 @@ -import type { StagingKWMv2Key } from '~/features/settings/keyFormats'; import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder'; import { formatHex } from './formatHex'; @@ -69,7 +68,11 @@ export class MMKVParser { return bytesToUTF8String(data).normalize(); } - public readOptionalString() { + public readKey() { + return this.readString(); + } + + public readStringValue(): string | null { // Container [ // len: int, // data: variant @@ -96,37 +99,4 @@ export class MMKVParser { const containerLen = this.readInt(); this.offset += containerLen; } - - public static toStringMap(view: DataView): Map { - const mmkv = new MMKVParser(view); - const result = new Map(); - while (!mmkv.eof) { - const key = mmkv.readString(); - const value = mmkv.readOptionalString(); - if (value) { - result.set(key, value); - } - } - return result; - } - - public static parseKuwoEKey(view: DataView): Omit[] { - const mmkv = new MMKVParser(view); - const result: Omit[] = []; - while (!mmkv.eof) { - const key = mmkv.readString(); - const idMatch = key.match(/^sec_ekey#(\d+)-(.+)/); - if (!idMatch) { - mmkv.skipContainer(); - continue; - } - - const [_, rid, quality] = idMatch; - const ekey = mmkv.readOptionalString(); - if (ekey) { - result.push({ rid, quality, ekey }); - } - } - return result; - } } diff --git a/src/util/__tests__/MMKVParser.test.ts b/src/util/__tests__/MMKVParser.test.ts index dc996ac..ff3d266 100644 --- a/src/util/__tests__/MMKVParser.test.ts +++ b/src/util/__tests__/MMKVParser.test.ts @@ -1,46 +1,28 @@ import { MMKVParser } from '../MMKVParser'; -import { readFileSync } from 'node:fs'; const makeViewFromBuffer = (buff: Buffer) => new DataView(buff.buffer.slice(buff.byteOffset, buff.byteOffset + buff.byteLength)); -test('parse qm mmkv file', () => { - const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/qm.mmkv')); - expect(Object.fromEntries(MMKVParser.toStringMap(view).entries())).toMatchInlineSnapshot(` - { - "Lorem Ipsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum congue volutpat metus non molestie. Quisque id est sapien. Fusce eget tristique sem. Donec tellus lacus, viverra sed lectus eget, elementum ultrices dolor. Integer non urna justo.", - "key": "value", - } - `); -}); - -test('parse qm mmkv file with optional str', () => { - const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/qm_optional.mmkv')); - expect(Object.fromEntries(MMKVParser.toStringMap(view).entries())).toMatchInlineSnapshot(` - { - "key": "value", - "key2": "value2", - } - `); -}); - -test('parse kuwo mmkv file', () => { - const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/kuwo.mmkv')); - expect(MMKVParser.parseKuwoEKey(view)).toMatchInlineSnapshot(` - [ - { - "ekey": "xyz123", - "quality": "20201kmflac", - "rid": "1234567", - }, - ] - `); -}); - test('throw error on broken file', () => { const view = makeViewFromBuffer( Buffer.from([0x27, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x6b, 0x65, 0x79, 0x06, 0x07, 0x62, 0x61, 0x64, 0xff, 0xff]), ); - expect(() => Object.fromEntries(MMKVParser.toStringMap(view).entries())).toThrow(/offset mismatch/i); + expect(() => { + const parser = new MMKVParser(view); + parser.readKey(); + parser.readStringValue(); + }).toThrow(/offset mismatch/i); +}); + +test('able to handle empty value', () => { + const view = makeViewFromBuffer( + Buffer.from([0x0b, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x6b, 0x65, 0x79, 0x00, 0x01, 0x31, 0x02, 0x01, 0x32, 0xff]), + ); + + const parser = new MMKVParser(view); + expect(parser.readKey()).toEqual('key'); + expect(parser.readStringValue()).toEqual(null); + expect(parser.readKey()).toEqual('1'); + expect(parser.readStringValue()).toEqual('2'); }); diff --git a/src/util/__tests__/__fixture__/qm_optional.mmkv b/src/util/__tests__/__fixture__/qm_optional.mmkv deleted file mode 100644 index 5e127d98a58d9199827dbea01f52f8be3afb7e77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48 wcmY#qU|^_c&Q7glV=YU}DNW@`%`GUYjL%HZ%P&f0U;#=Pv9p0?jQ)cF09)hC Mi8-aI4F4ej00N^H00000 literal 0 HcmV?d00001 diff --git a/src/util/__tests__/__fixture__/qm.mmkv b/src/util/mmkv/__tests__/__fixture__/qm.mmkv similarity index 77% rename from src/util/__tests__/__fixture__/qm.mmkv rename to src/util/mmkv/__tests__/__fixture__/qm.mmkv index 04a8a013432b3f44aea3464fb9d3ffdc855ad1b7..58459c3fba2c581064a5be4b39d860f387d40dd2 100644 GIT binary patch delta 10 RcmZ3$w1A0Gcq5}cBLEK70xJLj delta 10 RcmZ3$w1A0`XCtFLBLEJv0we$c diff --git a/src/util/mmkv/__tests__/kuwo.test.ts b/src/util/mmkv/__tests__/kuwo.test.ts new file mode 100644 index 0000000..d337914 --- /dev/null +++ b/src/util/mmkv/__tests__/kuwo.test.ts @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs'; +import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '../kuwo'; + +const makeViewFromBuffer = (buff: Buffer) => + new DataView(buff.buffer.slice(buff.byteOffset, buff.byteOffset + buff.byteLength)); + +test('parse kuwo android ekey mmkv file "cn.kuwo.player.mmkv.defaultconfig"', () => { + const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/kuwo_android.mmkv')); + expect(parseAndroidKuwoEKey(view)).toMatchInlineSnapshot(` + [ + { + "ekey": "xyz123", + "quality": "20201kmflac", + "rid": "1234567", + }, + ] + `); +}); + +test('parse kuwo ios ekey mmkv file "kw_ekey"', () => { + const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/kuwo_ios.mmkv')); + expect(parseIosKuwoEKey(view)).toMatchInlineSnapshot(` + [ + { + "ekey": "xyz123", + "quality": "20201", + "rid": "1234567", + }, + ] + `); +}); diff --git a/src/util/mmkv/__tests__/qm.test.ts b/src/util/mmkv/__tests__/qm.test.ts new file mode 100644 index 0000000..17abe18 --- /dev/null +++ b/src/util/mmkv/__tests__/qm.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from 'node:fs'; +import { parseAndroidQmEKey } from '../qm'; + +const makeViewFromBuffer = (buff: Buffer) => + new DataView(buff.buffer.slice(buff.byteOffset, buff.byteOffset + buff.byteLength)); + +test('parse qm mmkv file', () => { + const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/qm.mmkv')); + expect(Object.fromEntries(parseAndroidQmEKey(view).entries())).toMatchInlineSnapshot(` + { + "Lorem Ipsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum congue volutpat metus non molestie. Quisque id est sapien. Fusce eget tristique sem. Donec tellus lacus, viverra sed lectus eget, elementum ultrices dolor. Integer non urna justo.", + "key": "value", + } + `); +}); diff --git a/src/util/mmkv/kuwo.ts b/src/util/mmkv/kuwo.ts new file mode 100644 index 0000000..bd22e14 --- /dev/null +++ b/src/util/mmkv/kuwo.ts @@ -0,0 +1,41 @@ +import type { StagingKWMv2Key } from '~/features/settings/keyFormats'; +import { MMKVParser } from '../MMKVParser'; + +export function parseAndroidKuwoEKey(view: DataView): Omit[] { + const mmkv = new MMKVParser(view); + const result: Omit[] = []; + while (!mmkv.eof) { + const key = mmkv.readString(); + const idMatch = key.match(/^sec_ekey#(\d+)-(\w+)$/); + if (!idMatch) { + mmkv.skipContainer(); + continue; + } + + const [_, rid, quality] = idMatch; + const ekey = mmkv.readStringValue(); + if (ekey) { + result.push({ rid, quality, ekey }); + } + } + return result; +} + +export function parseIosKuwoEKey(view: DataView): Omit[] { + const mmkv = new MMKVParser(view); + const result: Omit[] = []; + while (!mmkv.eof) { + const key = mmkv.readKey(); + const idMatch = key.match(/^(\d+)_(\d+)$/); + if (!idMatch) { + mmkv.skipContainer(); + continue; + } + const [_, rid, quality] = idMatch; + const ekey = mmkv.readStringValue(); + if (ekey) { + result.push({ rid, quality, ekey }); + } + } + return result; +} diff --git a/src/util/mmkv/qm.ts b/src/util/mmkv/qm.ts new file mode 100644 index 0000000..5f711f4 --- /dev/null +++ b/src/util/mmkv/qm.ts @@ -0,0 +1,14 @@ +import { MMKVParser } from '../MMKVParser'; + +export function parseAndroidQmEKey(view: DataView): Map { + const mmkv = new MMKVParser(view); + const result = new Map(); + while (!mmkv.eof) { + const key = mmkv.readString(); + const value = mmkv.readStringValue(); + if (value) { + result.set(key, value); + } + } + return result; +} diff --git a/vite.config.ts b/vite.config.ts index 1d83f70..f66536c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -114,7 +114,6 @@ export default defineConfig({ ], // workaround: sql.js is not ESModule friendly, yet... deps: { - // inline: ['sql.js'], optimizer: { web: { include: ['sql.js'], From 6365d7a0342a8464ea0d34535b7b94eadb932b2d Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Wed, 8 Nov 2023 20:49:13 +0000 Subject: [PATCH 2/2] fix: fix bad refactor --- src/features/settings/panels/PanelQMCv2Key.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/settings/panels/PanelQMCv2Key.tsx b/src/features/settings/panels/PanelQMCv2Key.tsx index 5f59502..6b37517 100644 --- a/src/features/settings/panels/PanelQMCv2Key.tsx +++ b/src/features/settings/panels/PanelQMCv2Key.tsx @@ -29,7 +29,7 @@ import { InfoOutlineIcon } from '@chakra-ui/icons'; import { ImportSecretModal } from '~/components/ImportSecretModal'; import { StagingQMCv2Key } from '../keyFormats'; import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor'; -import { MMKVParser } from '~/util/MMKVParser'; +import { parseAndroidQmEKey } from '~/util/mmkv/qm'; import { getFileName } from '~/util/pathHelper'; import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions'; import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions'; @@ -63,7 +63,7 @@ export function PanelQMCv2Key() { } } else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) { const fileBuffer = await file.arrayBuffer(); - const map = MMKVParser.toStringMap(new DataView(fileBuffer)); + const map = parseAndroidQmEKey(new DataView(fileBuffer)); qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey })); }