Compare commits
4 Commits
c25dffa778
...
1a8e7db9be
Author | SHA1 | Date | |
---|---|---|---|
1a8e7db9be | |||
c7846ec15b | |||
fe256218e6 | |||
65b2474b43 |
@ -21,7 +21,7 @@
|
|||||||
"@chakra-ui/react": "^2.7.0",
|
"@chakra-ui/react": "^2.7.0",
|
||||||
"@emotion/react": "^11.11.0",
|
"@emotion/react": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@jixun/libparakeet": "0.1.2",
|
"@jixun/libparakeet": "0.2.0",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"framer-motion": "^10.12.16",
|
"framer-motion": "^10.12.16",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
|
792
pnpm-lock.yaml
792
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
25
src/crypto/pasreKuwo.ts
Normal file
25
src/crypto/pasreKuwo.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
||||||
|
import { strlen } from './strlen';
|
||||||
|
|
||||||
|
export interface KuwoHeader {
|
||||||
|
rid: string; // uint64
|
||||||
|
encVersion: 1 | 2; // uint32
|
||||||
|
quality: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseKuwoHeader(view: DataView): KuwoHeader | null {
|
||||||
|
const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10);
|
||||||
|
if (bytesToUTF8String(magic) !== 'yeelion-kuwo-tme') {
|
||||||
|
return null; // not kuwo-encrypted file
|
||||||
|
}
|
||||||
|
|
||||||
|
const qualityBytes = new Uint8Array(view.buffer.slice(view.byteOffset + 0x30, view.byteOffset + 0x40));
|
||||||
|
const qualityLen = strlen(qualityBytes);
|
||||||
|
const quality = bytesToUTF8String(qualityBytes.slice(0, qualityLen));
|
||||||
|
|
||||||
|
return {
|
||||||
|
encVersion: view.getUint32(0x10, true) as 1 | 2,
|
||||||
|
rid: view.getUint32(0x18, true).toString(),
|
||||||
|
quality,
|
||||||
|
};
|
||||||
|
}
|
9
src/crypto/strlen.ts
Normal file
9
src/crypto/strlen.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function strlen(data: Uint8Array): number {
|
||||||
|
const n = data.byteLength;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (data[i] === 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
@ -1,14 +1,25 @@
|
|||||||
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
||||||
import type { CryptoBase } from '../CryptoBase';
|
import type { CryptoBase } from '../CryptoBase';
|
||||||
import { KWM_KEY } from './kwm.key';
|
import { KWM_KEY } from './kwm.key';
|
||||||
|
import { DecryptCommandOptions } from '~/decrypt-worker/types';
|
||||||
|
import { makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto';
|
||||||
|
import { fetchParakeet } from '@jixun/libparakeet';
|
||||||
|
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder';
|
||||||
|
|
||||||
// v1 only
|
// v1 only
|
||||||
export class KWMCrypto implements CryptoBase {
|
export class KWMCrypto implements CryptoBase {
|
||||||
cryptoName = 'KWM';
|
cryptoName = 'KWM';
|
||||||
checkByDecryptHeader = true;
|
checkByDecryptHeader = true;
|
||||||
|
|
||||||
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
|
async decrypt(buffer: ArrayBuffer, opts: DecryptCommandOptions): Promise<Blob> {
|
||||||
return transformBlob(buffer, (p) => p.make.KuwoKWM(KWM_KEY));
|
const kwm2key = opts.kwm2key ?? '';
|
||||||
|
|
||||||
|
const parakeet = await fetchParakeet();
|
||||||
|
const keyCrypto = makeQMCv2KeyCrypto(parakeet);
|
||||||
|
return transformBlob(buffer, (p) => p.make.KuwoKWMv2(KWM_KEY, stringToUTF8Bytes(kwm2key), keyCrypto), {
|
||||||
|
cleanup: () => keyCrypto.delete(),
|
||||||
|
parakeet,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static make() {
|
public static make() {
|
||||||
|
@ -3,6 +3,8 @@ import type { CryptoBase } from '../CryptoBase';
|
|||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||||
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts';
|
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts';
|
||||||
import { fetchParakeet } from '@jixun/libparakeet';
|
import { fetchParakeet } from '@jixun/libparakeet';
|
||||||
|
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts';
|
||||||
|
import { makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts';
|
||||||
|
|
||||||
export class QMC2Crypto implements CryptoBase {
|
export class QMC2Crypto implements CryptoBase {
|
||||||
cryptoName = 'QMC/v2';
|
cryptoName = 'QMC/v2';
|
||||||
@ -36,9 +38,8 @@ export class QMC2CryptoWithKey implements CryptoBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parakeet = await fetchParakeet();
|
const parakeet = await fetchParakeet();
|
||||||
const textEncoder = new TextEncoder();
|
const key = stringToUTF8Bytes(options.qmc2Key);
|
||||||
const key = textEncoder.encode(options.qmc2Key);
|
const keyCrypto = makeQMCv2KeyCrypto(parakeet);
|
||||||
const keyCrypto = parakeet.make.QMCv2KeyCrypto(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
|
|
||||||
return transformBlob(buffer, (p) => p.make.QMCv2EKey(key, keyCrypto), {
|
return transformBlob(buffer, (p) => p.make.QMCv2EKey(key, keyCrypto), {
|
||||||
parakeet,
|
parakeet,
|
||||||
cleanup: () => keyCrypto.delete(),
|
cleanup: () => keyCrypto.delete(),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface DecryptCommandOptions {
|
export interface DecryptCommandOptions {
|
||||||
qmc2Key?: string;
|
qmc2Key?: string;
|
||||||
|
kwm2key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DecryptCommandPayload {
|
export interface DecryptCommandPayload {
|
||||||
|
4
src/decrypt-worker/util/qmc2KeyCrypto.ts
Normal file
4
src/decrypt-worker/util/qmc2KeyCrypto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import type { Parakeet } from '@jixun/libparakeet';
|
||||||
|
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from '../crypto/qmc/qmc_v2.key';
|
||||||
|
|
||||||
|
export const makeQMCv2KeyCrypto = (p: Parakeet) => p.make.QMCv2KeyCrypto(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
|
10
src/decrypt-worker/util/utf8Encoder.ts
Normal file
10
src/decrypt-worker/util/utf8Encoder.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const utf8Encoder = new TextEncoder();
|
||||||
|
export const utf8Decoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
export function stringToUTF8Bytes(str: string) {
|
||||||
|
return utf8Encoder.encode(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToUTF8String(str: BufferSource) {
|
||||||
|
return utf8Decoder.decode(str);
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import type { RootState } from '~/store';
|
import type { RootState } from '~/store';
|
||||||
import { decryptionQueue } from '~/decrypt-worker/client';
|
|
||||||
|
|
||||||
import type { DecryptionResult } from '~/decrypt-worker/constants';
|
import type { DecryptionResult } from '~/decrypt-worker/constants';
|
||||||
|
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
||||||
|
import { decryptionQueue } from '~/decrypt-worker/client';
|
||||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||||
import { selectDecryptOptionByFile } from '../settings/settingsSelector';
|
import { selectQMCv2KeyByFileName, selectKWMv2Key } from '../settings/settingsSelector';
|
||||||
|
|
||||||
export enum ProcessState {
|
export enum ProcessState {
|
||||||
QUEUED = 'QUEUED',
|
QUEUED = 'QUEUED',
|
||||||
@ -63,7 +64,18 @@ export const processFile = createAsyncThunk<
|
|||||||
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = selectDecryptOptionByFile(state, file.fileName);
|
const fileHeader = await fetch(file.raw, {
|
||||||
|
headers: {
|
||||||
|
Range: 'bytes=0-1023',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((r) => r.blob())
|
||||||
|
.then((r) => r.arrayBuffer());
|
||||||
|
|
||||||
|
const options: DecryptCommandOptions = {
|
||||||
|
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
|
||||||
|
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
||||||
|
};
|
||||||
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,6 +51,13 @@ export interface StagingKWMv2Key {
|
|||||||
|
|
||||||
export type ProductionKWMv2Keys = Record<string /* `${rid}-${quality}` */, string /* ekey */>;
|
export type ProductionKWMv2Keys = Record<string /* `${rid}-${quality}` */, string /* ekey */>;
|
||||||
|
|
||||||
|
export const parseKwm2ProductionKey = (key: string): null | { rid: string; quality: string } => {
|
||||||
|
const m = key.match(/^(\d+)-(\w+)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
const [_, rid, quality] = m;
|
||||||
|
|
||||||
|
return { rid, quality };
|
||||||
|
};
|
||||||
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality}`;
|
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality}`;
|
||||||
export const kwm2StagingToProductionValue = (key: StagingKWMv2Key) => key.ekey;
|
export const kwm2StagingToProductionValue = (key: StagingKWMv2Key) => key.ekey;
|
||||||
export const kwm2ProductionToStaging = (
|
export const kwm2ProductionToStaging = (
|
||||||
@ -59,9 +66,8 @@ export const kwm2ProductionToStaging = (
|
|||||||
): null | StagingKWMv2Key => {
|
): null | StagingKWMv2Key => {
|
||||||
if (typeof value !== 'string') return null;
|
if (typeof value !== 'string') return null;
|
||||||
|
|
||||||
const m = key.match(/^(\d+)-(\w+)$/);
|
const parsed = parseKwm2ProductionKey(key);
|
||||||
if (!m) return null;
|
if (!parsed) return null;
|
||||||
|
|
||||||
const [_, rid, quality] = m;
|
return { id: nanoid(), rid: parsed.rid, quality: parsed.quality, ekey: value };
|
||||||
return { id: nanoid(), rid, quality, ekey: value };
|
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import type { AppStore } from '~/store';
|
|||||||
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
||||||
import { enumObject } from '~/util/objects';
|
import { enumObject } from '~/util/objects';
|
||||||
import { getLogger } from '~/util/logUtils';
|
import { getLogger } from '~/util/logUtils';
|
||||||
|
import { parseKwm2ProductionKey } from './keyFormats';
|
||||||
|
|
||||||
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
||||||
|
|
||||||
@ -22,6 +23,16 @@ function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
|||||||
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings?.kwm2) {
|
||||||
|
const { keys } = settings.kwm2;
|
||||||
|
|
||||||
|
for (const [k, v] of enumObject(keys)) {
|
||||||
|
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
||||||
|
draft.kwm2.keys[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,47 @@
|
|||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
import { parseKuwoHeader } from '~/crypto/pasreKuwo';
|
||||||
import type { RootState } from '~/store';
|
import type { RootState } from '~/store';
|
||||||
import { closestByLevenshtein } from '~/util/levenshtein';
|
import { closestByLevenshtein } from '~/util/levenshtein';
|
||||||
import { hasOwn } from '~/util/objects';
|
import { hasOwn } from '~/util/objects';
|
||||||
|
import { kwm2StagingToProductionKey } from './keyFormats';
|
||||||
|
|
||||||
export const selectStagingQMCv2Settings = (state: RootState) => state.settings.staging.qmc2;
|
export const selectStagingQMCv2Settings = (state: RootState) => state.settings.staging.qmc2;
|
||||||
export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2;
|
export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2;
|
||||||
|
|
||||||
export const selectStagingKWMv2Keys = (state: RootState) => state.settings.staging.kwm2.keys;
|
export const selectStagingKWMv2Keys = (state: RootState) => state.settings.staging.kwm2.keys;
|
||||||
|
export const selectFinalKWMv2Keys = (state: RootState) => state.settings.production.kwm2.keys;
|
||||||
|
|
||||||
export const selectDecryptOptionByFile = (state: RootState, name: string): DecryptCommandOptions => {
|
export const selectQMCv2KeyByFileName = (state: RootState, name: string): string | undefined => {
|
||||||
const normalizedName = name.normalize();
|
const normalizedName = name.normalize();
|
||||||
|
|
||||||
let qmc2Key: string | undefined;
|
let ekey: string | undefined;
|
||||||
const { keys: qmc2Keys, allowFuzzyNameSearch } = selectFinalQMCv2Settings(state);
|
const { keys, allowFuzzyNameSearch } = selectFinalQMCv2Settings(state);
|
||||||
if (hasOwn(qmc2Keys, normalizedName)) {
|
if (hasOwn(keys, normalizedName)) {
|
||||||
qmc2Key = qmc2Keys[normalizedName];
|
ekey = keys[normalizedName];
|
||||||
} else if (allowFuzzyNameSearch) {
|
} else if (allowFuzzyNameSearch) {
|
||||||
const qmc2KeyStoreNames = Object.keys(qmc2Keys);
|
const qmc2KeyStoreNames = Object.keys(keys);
|
||||||
if (qmc2KeyStoreNames.length > 0) {
|
if (qmc2KeyStoreNames.length > 0) {
|
||||||
const closestName = closestByLevenshtein(normalizedName, qmc2KeyStoreNames);
|
const closestName = closestByLevenshtein(normalizedName, qmc2KeyStoreNames);
|
||||||
console.debug('qmc2: key db could not find %o, using closest %o instead.', normalizedName, closestName);
|
console.debug('qmc2: key db could not find %o, using closest %o instead.', normalizedName, closestName);
|
||||||
qmc2Key = qmc2Keys[closestName];
|
ekey = keys[closestName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return ekey;
|
||||||
qmc2Key,
|
};
|
||||||
};
|
|
||||||
|
export const selectKWMv2Key = (state: RootState, headerView: DataView): string | undefined => {
|
||||||
|
const hdr = parseKuwoHeader(headerView);
|
||||||
|
if (!hdr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = selectFinalKWMv2Keys(state);
|
||||||
|
const lookupKey = kwm2StagingToProductionKey({ id: '', ekey: '', quality: hdr.quality, rid: hdr.rid });
|
||||||
|
|
||||||
|
let ekey: string | undefined;
|
||||||
|
if (hasOwn(keys, lookupKey)) {
|
||||||
|
ekey = keys[lookupKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ekey;
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { StagingKWMv2Key } from '~/features/settings/keyFormats';
|
import type { StagingKWMv2Key } from '~/features/settings/keyFormats';
|
||||||
|
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
||||||
import { formatHex } from './formatHex';
|
import { formatHex } from './formatHex';
|
||||||
|
|
||||||
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
||||||
|
|
||||||
export class MMKVParser {
|
export class MMKVParser {
|
||||||
private offset = 4;
|
private offset = 4;
|
||||||
private length: number;
|
private length: number;
|
||||||
@ -67,7 +66,7 @@ export class MMKVParser {
|
|||||||
// ]
|
// ]
|
||||||
const strByteLen = this.readInt();
|
const strByteLen = this.readInt();
|
||||||
const data = this.readBytes(strByteLen);
|
const data = this.readBytes(strByteLen);
|
||||||
return textDecoder.decode(data).normalize();
|
return bytesToUTF8String(data).normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public readVariantString() {
|
public readVariantString() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const textEncoder = new TextEncoder();
|
|
||||||
|
|
||||||
// translation of pseudocode from Wikipedia:
|
// translation of pseudocode from Wikipedia:
|
||||||
|
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder';
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||||
export function levenshtein(str1: string, str2: string) {
|
export function levenshtein(str1: string, str2: string) {
|
||||||
if (str1 === str2) {
|
if (str1 === str2) {
|
||||||
@ -14,8 +14,8 @@ export function levenshtein(str1: string, str2: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert them to Uint8Array to avoid expensive string APIs.
|
// Convert them to Uint8Array to avoid expensive string APIs.
|
||||||
const s = textEncoder.encode(str1.toLowerCase());
|
const s = stringToUTF8Bytes(str1.normalize().toLowerCase());
|
||||||
const t = textEncoder.encode(str2.toLowerCase());
|
const t = stringToUTF8Bytes(str2.normalize().toLowerCase());
|
||||||
const m = s.byteLength;
|
const m = s.byteLength;
|
||||||
const n = t.byteLength;
|
const n = t.byteLength;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user