From 80dbeeb5459519e07ff8b5ccd10255e98e76d70b Mon Sep 17 00:00:00 2001 From: daydreamer-json Date: Mon, 30 Mar 2026 17:21:08 +0900 Subject: [PATCH] feat: implement resource index decryption - Add `cipher` utility for resource index decryption/encryption - Automatically decrypt `index_initial.json` and `index_main.json` during archiving - pages: Update `ResourcesTab` to display links to decrypted versions of index files - Update README to reflect that some raw data is now decrypted --- README.md | 2 +- pages-v2/src/components/tabs/ResourcesTab.tsx | 19 ++++++++++--- src/cmds/archive.ts | 21 ++++++++++++++ src/utils/cipher.ts | 28 +++++++++++++++++++ src/utils/config.ts | 10 +++++++ 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/utils/cipher.ts diff --git a/README.md b/README.md index b5bbda3..45aeb6f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The APIs currently being monitored are as follows: - Raw - Game resource json (index, patch) -Currently, raw data is provided as-is without modification. It is also not decrypted. +Most of the raw data is provided as-is, without any modifications. Some data (`index_*.json`) has been decrypted. ## Download Library diff --git a/pages-v2/src/components/tabs/ResourcesTab.tsx b/pages-v2/src/components/tabs/ResourcesTab.tsx index 6b688a4..a13ca8b 100644 --- a/pages-v2/src/components/tabs/ResourcesTab.tsx +++ b/pages-v2/src/components/tabs/ResourcesTab.tsx @@ -140,7 +140,7 @@ function processRawData(rawData: StoredData { +const ResourceLink = ({ basePath, file, isDecExist }: { basePath: string; file: string; isDecExist: boolean }) => { const fullPath = `${basePath}/${file}`; return ( <> @@ -151,6 +151,17 @@ const ResourceLink = ({ basePath, file }: { basePath: string; file: string }) => Mirror + {isDecExist ? ( + <> + {' '} + /{' '} + + Dec + + + ) : ( + '' + )} ); }; @@ -186,13 +197,13 @@ const ResourceTable = ({ groups }: { groups: ResourceGroup[] }) => ( {group.versions.join(', ')} {group.isKick ? '✅' : ''} - + - + - + ))} diff --git a/src/cmds/archive.ts b/src/cmds/archive.ts index 7f3446c..c2cd12c 100644 --- a/src/cmds/archive.ts +++ b/src/cmds/archive.ts @@ -5,6 +5,7 @@ import PQueue from 'p-queue'; import semver from 'semver'; import apiUtils from '../utils/api/index.js'; import argvUtils from '../utils/argv.js'; +import cipher from '../utils/cipher.js'; import appConfig from '../utils/config.js'; import logger from '../utils/logger.js'; import mathUtils from '../utils/math.js'; @@ -487,6 +488,26 @@ async function fetchAndSaveAllGameResRawData(gameTargets: GameTarget[]) { for (const url of webAssetUrls) addToQueue(url); await networkQueue.onIdle(); + + // res index decryption + for (const url of resourceUrls) { + const urlObj = new URL(url); + urlObj.search = ''; + if (['index_initial.json', 'index_main.json'].includes(urlObj.pathname.split('/').pop()!) === false) continue; + const localPath = path.join( + argvUtils.getArgv()['outputDir'], + 'raw', + urlObj.hostname, + ...urlObj.pathname.split('/').filter(Boolean), + ); + const localPathDec = localPath.replace(/\.json$/, '_dec.json'); + if (!(await Bun.file(localPathDec).exists()) && (await Bun.file(localPath).exists())) { + const encBytes = new Uint8Array(Buffer.from(await Bun.file(localPath).text(), 'base64')); + const decBytes = cipher.decryptResIndex(encBytes, appConfig.cipher.akEndfield.resIndexKey); + await Bun.write(localPathDec, decBytes); + } + } + logger.info(`Fetched raw game resources: ${wroteFiles.length} files`); } diff --git a/src/utils/cipher.ts b/src/utils/cipher.ts new file mode 100644 index 0000000..f75c1e3 --- /dev/null +++ b/src/utils/cipher.ts @@ -0,0 +1,28 @@ +function decryptResIndex(encData: Uint8Array, key: string): Uint8Array { + const keyBytes = Buffer.from(key, 'utf-8'); + const keyLength = keyBytes.length; + const result = new Uint8Array(encData.length); + for (let i = 0; i < encData.length; i++) { + const encByte = encData[i]!; + const keyByte = keyBytes[i % keyLength]!; + result[i] = (encByte - keyByte + 256) % 256; + } + return result; +} + +function encryptResIndex(plainData: Uint8Array, key: string): Uint8Array { + const keyBytes = Buffer.from(key, 'utf-8'); + const keyLength = keyBytes.length; + const result = new Uint8Array(plainData.length); + for (let i = 0; i < plainData.length; i++) { + const plainByte = plainData[i]!; + const keyByte = keyBytes[i % keyLength]!; + result[i] = (plainByte + keyByte) % 256; + } + return result; +} + +export default { + decryptResIndex, + encryptResIndex, +}; diff --git a/src/utils/config.ts b/src/utils/config.ts index a06a67e..a4f27f6 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -12,6 +12,11 @@ type AllRequired = Required<{ type ConfigType = AllRequired< Freeze<{ + cipher: { + akEndfield: { + resIndexKey: string; + }; + }; network: { api: { akEndfield: { @@ -69,6 +74,11 @@ type ConfigType = AllRequired< >; const initialConfig: ConfigType = { + cipher: { + akEndfield: { + resIndexKey: 'Assets/Beyond/DynamicAssets/Gameplay/UI/Fonts/', // via reversing + }, + }, network: { api: { akEndfield: {