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
This commit is contained in:
daydreamer-json
2026-03-30 17:21:08 +09:00
parent 75899425a1
commit 80dbeeb545
5 changed files with 75 additions and 5 deletions

View File

@@ -27,7 +27,7 @@ The APIs currently being monitored are as follows:
- Raw - Raw
- Game resource json (index, patch) - 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 ## Download Library

View File

@@ -140,7 +140,7 @@ function processRawData(rawData: StoredData<IApiEndfield.LauncherLatestGameResou
}); });
} }
const ResourceLink = ({ basePath, file }: { basePath: string; file: string }) => { const ResourceLink = ({ basePath, file, isDecExist }: { basePath: string; file: string; isDecExist: boolean }) => {
const fullPath = `${basePath}/${file}`; const fullPath = `${basePath}/${file}`;
return ( return (
<> <>
@@ -151,6 +151,17 @@ const ResourceLink = ({ basePath, file }: { basePath: string; file: string }) =>
<a href={getMirrorUrl(fullPath)} target='_blank' rel='noreferrer'> <a href={getMirrorUrl(fullPath)} target='_blank' rel='noreferrer'>
Mirror Mirror
</a> </a>
{isDecExist ? (
<>
{' '}
/{' '}
<a href={getMirrorUrl(fullPath).replace(/\.json$/, '_dec.json')} target='_blank' rel='noreferrer'>
Dec
</a>
</>
) : (
''
)}
</> </>
); );
}; };
@@ -186,13 +197,13 @@ const ResourceTable = ({ groups }: { groups: ResourceGroup[] }) => (
<td>{group.versions.join(', ')}</td> <td>{group.versions.join(', ')}</td>
<td className='text-center'>{group.isKick ? '✅' : ''}</td> <td className='text-center'>{group.isKick ? '✅' : ''}</td>
<td> <td>
<ResourceLink basePath={group.initialRes.path} file='index_initial.json' /> <ResourceLink basePath={group.initialRes.path} file='index_initial.json' isDecExist={true} />
</td> </td>
<td> <td>
<ResourceLink basePath={group.mainRes.path} file='index_main.json' /> <ResourceLink basePath={group.mainRes.path} file='index_main.json' isDecExist={true} />
</td> </td>
<td> <td>
<ResourceLink basePath={group.mainRes.path} file='patch.json' /> <ResourceLink basePath={group.mainRes.path} file='patch.json' isDecExist={false} />
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -5,6 +5,7 @@ import PQueue from 'p-queue';
import semver from 'semver'; import semver from 'semver';
import apiUtils from '../utils/api/index.js'; import apiUtils from '../utils/api/index.js';
import argvUtils from '../utils/argv.js'; import argvUtils from '../utils/argv.js';
import cipher from '../utils/cipher.js';
import appConfig from '../utils/config.js'; import appConfig from '../utils/config.js';
import logger from '../utils/logger.js'; import logger from '../utils/logger.js';
import mathUtils from '../utils/math.js'; import mathUtils from '../utils/math.js';
@@ -487,6 +488,26 @@ async function fetchAndSaveAllGameResRawData(gameTargets: GameTarget[]) {
for (const url of webAssetUrls) addToQueue(url); for (const url of webAssetUrls) addToQueue(url);
await networkQueue.onIdle(); 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`); logger.info(`Fetched raw game resources: ${wroteFiles.length} files`);
} }

28
src/utils/cipher.ts Normal file
View File

@@ -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,
};

View File

@@ -12,6 +12,11 @@ type AllRequired<T> = Required<{
type ConfigType = AllRequired< type ConfigType = AllRequired<
Freeze<{ Freeze<{
cipher: {
akEndfield: {
resIndexKey: string;
};
};
network: { network: {
api: { api: {
akEndfield: { akEndfield: {
@@ -69,6 +74,11 @@ type ConfigType = AllRequired<
>; >;
const initialConfig: ConfigType = { const initialConfig: ConfigType = {
cipher: {
akEndfield: {
resIndexKey: 'Assets/Beyond/DynamicAssets/Gameplay/UI/Fonts/', // via reversing
},
},
network: { network: {
api: { api: {
akEndfield: { akEndfield: {