Compare commits
No commits in common. "85a5fc3e7ee9ef2fc719ccdcfe05c326dbc9b0c4" and "75cc18477c1fbe37aaf0267e24149d20556c8037" have entirely different histories.
85a5fc3e7e
...
75cc18477c
@ -7,11 +7,9 @@ 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 (!KUWO_MAGIC_HDRS.has(bytesToUTF8String(magic))) {
|
||||
if (bytesToUTF8String(magic) !== 'yeelion-kuwo-tme') {
|
||||
return null; // not kuwo-encrypted file
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { objectify } from 'radash';
|
||||
|
||||
export function productionKeyToStaging<S, P extends Record<string, unknown>>(
|
||||
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,13 +44,7 @@ export const qmc2ProductionToStaging = (
|
||||
|
||||
export interface StagingKWMv2Key {
|
||||
id: string;
|
||||
/**
|
||||
* Resource ID
|
||||
*/
|
||||
rid: string;
|
||||
/**
|
||||
* Quality String
|
||||
*/
|
||||
quality: string;
|
||||
ekey: string;
|
||||
}
|
||||
@ -64,17 +58,16 @@ export const parseKwm2ProductionKey = (key: string): null | { rid: string; quali
|
||||
|
||||
return { rid, quality };
|
||||
};
|
||||
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality.replace(/[\D]/g, '')}`;
|
||||
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality}`;
|
||||
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, quality, ekey: value };
|
||||
return { id: nanoid(), rid: parsed.rid, quality: parsed.quality, ekey: value };
|
||||
};
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { Code, ListItem, OrderedList, Text, chakra } from '@chakra-ui/react';
|
||||
|
||||
const KUWO_IOS_DIR = '/var/mobile/Containers/Data/Application/<酷我数据目录>/mmkv';
|
||||
|
||||
export function InstructionsIOS() {
|
||||
return (
|
||||
<>
|
||||
<Text>你需要越狱来访问 iOS 应用的私有数据。</Text>
|
||||
<Text>
|
||||
⚠️ 请注意,越狱通常意味着你的设备
|
||||
<chakra.span color="red.400">将失去保修资格</chakra.span>。
|
||||
</Text>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
访问设备的这个目录:
|
||||
<Code wordBreak="break-word">{KUWO_IOS_DIR}</Code>
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
提取密钥数据库文件 <Code>kw_ekey</Code> 至浏览器可访问的目录,如下载目录。
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
提交刚刚提取的 <Code>kw_ekey</Code> 密钥数据库。
|
||||
</Text>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
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 (
|
||||
<>
|
||||
<TabList>
|
||||
<Tab>安卓</Tab>
|
||||
<Tab>iOS</Tab>
|
||||
<Tab>Windows</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex={1} overflow="auto">
|
||||
@ -18,9 +16,6 @@ export function KWMv2AllInstructions() {
|
||||
file="cn.kuwo.player.mmkv.defaultconfig"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<InstructionsIOS />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<InstructionsPC />
|
||||
</TabPanel>
|
||||
|
@ -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 { parseAndroidKuwoEKey, parseIosKuwoEKey } from '~/util/mmkv/kuwo';
|
||||
import { MMKVParser } from '~/util/MMKVParser';
|
||||
|
||||
import { kwm2AddKey, kwm2ClearKeys, kwm2ImportKeys } from '../settingsSlice';
|
||||
import { selectStagingKWMv2Keys } from '../settingsSelector';
|
||||
@ -41,11 +41,9 @@ export function PanelKWMv2Key() {
|
||||
const handleSecretImport = async (file: File) => {
|
||||
let keys: Omit<StagingKWMv2Key, 'id'>[] | null = null;
|
||||
if (/cn\.kuwo\.player\.mmkv/i.test(file.name)) {
|
||||
keys = parseAndroidKuwoEKey(new DataView(await file.arrayBuffer()));
|
||||
} else if (/kw_ekey/.test(file.name)) {
|
||||
keys = parseIosKuwoEKey(new DataView(await file.arrayBuffer()));
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
keys = MMKVParser.parseKuwoEKey(new DataView(fileBuffer));
|
||||
}
|
||||
|
||||
if (keys?.length === 0) {
|
||||
toast({
|
||||
title: '未导入密钥',
|
||||
|
@ -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 { parseAndroidQmEKey } from '~/util/mmkv/qm';
|
||||
import { MMKVParser } from '~/util/MMKVParser';
|
||||
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 = parseAndroidQmEKey(new DataView(fileBuffer));
|
||||
const map = MMKVParser.toStringMap(new DataView(fileBuffer));
|
||||
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,6 @@ 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: {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { StagingKWMv2Key } from '~/features/settings/keyFormats';
|
||||
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
||||
import { formatHex } from './formatHex';
|
||||
|
||||
@ -68,11 +69,7 @@ export class MMKVParser {
|
||||
return bytesToUTF8String(data).normalize();
|
||||
}
|
||||
|
||||
public readKey() {
|
||||
return this.readString();
|
||||
}
|
||||
|
||||
public readStringValue(): string | null {
|
||||
public readOptionalString() {
|
||||
// Container [
|
||||
// len: int,
|
||||
// data: variant
|
||||
@ -99,4 +96,37 @@ export class MMKVParser {
|
||||
const containerLen = this.readInt();
|
||||
this.offset += containerLen;
|
||||
}
|
||||
|
||||
public static toStringMap(view: DataView): Map<string, string> {
|
||||
const mmkv = new MMKVParser(view);
|
||||
const result = new Map<string, string>();
|
||||
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<StagingKWMv2Key, 'id'>[] {
|
||||
const mmkv = new MMKVParser(view);
|
||||
const result: Omit<StagingKWMv2Key, 'id'>[] = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,46 @@
|
||||
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(() => {
|
||||
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');
|
||||
expect(() => Object.fromEntries(MMKVParser.toStringMap(view).entries())).toThrow(/offset mismatch/i);
|
||||
});
|
||||
|
Binary file not shown.
BIN
src/util/__tests__/__fixture__/qm_optional.mmkv
Normal file
BIN
src/util/__tests__/__fixture__/qm_optional.mmkv
Normal file
Binary file not shown.
Binary file not shown.
@ -1,31 +0,0 @@
|
||||
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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
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",
|
||||
}
|
||||
`);
|
||||
});
|
@ -1,41 +0,0 @@
|
||||
import type { StagingKWMv2Key } from '~/features/settings/keyFormats';
|
||||
import { MMKVParser } from '../MMKVParser';
|
||||
|
||||
export function parseAndroidKuwoEKey(view: DataView): Omit<StagingKWMv2Key, 'id'>[] {
|
||||
const mmkv = new MMKVParser(view);
|
||||
const result: Omit<StagingKWMv2Key, 'id'>[] = [];
|
||||
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<StagingKWMv2Key, 'id'>[] {
|
||||
const mmkv = new MMKVParser(view);
|
||||
const result: Omit<StagingKWMv2Key, 'id'>[] = [];
|
||||
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;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { MMKVParser } from '../MMKVParser';
|
||||
|
||||
export function parseAndroidQmEKey(view: DataView): Map<string, string> {
|
||||
const mmkv = new MMKVParser(view);
|
||||
const result = new Map<string, string>();
|
||||
while (!mmkv.eof) {
|
||||
const key = mmkv.readString();
|
||||
const value = mmkv.readStringValue();
|
||||
if (value) {
|
||||
result.set(key, value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
@ -114,6 +114,7 @@ export default defineConfig({
|
||||
],
|
||||
// workaround: sql.js is not ESModule friendly, yet...
|
||||
deps: {
|
||||
// inline: ['sql.js'],
|
||||
optimizer: {
|
||||
web: {
|
||||
include: ['sql.js'],
|
||||
|
Loading…
Reference in New Issue
Block a user