mirror of
https://github.com/daydreamer-json/ak-endfield-api-archive.git
synced 2026-04-01 05:02:26 +02:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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
28
src/utils/cipher.ts
Normal 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,
|
||||||
|
};
|
||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user