feat: Add support for importing encrypted files

This commit is contained in:
mrjvs 2025-09-14 19:22:46 +02:00
parent ce4eec54bc
commit dc471eefa9
4 changed files with 74 additions and 5 deletions

View File

@ -10,6 +10,7 @@ npm run cli -- import seed
1. Only files referenced by tasksheets are processed
2. You can safely run seeding as many times as seeded, it ignores unchanged data.
3. Files must be prefixed with their Data ID - followed by a `.`. For example: `39015.Festival.byaml` (Everything after the first dot is not enforced, name it appropiately)
4. Tasksheets must follow this syntax: `1.<BOSS_APP_ID>.<TASKNAME>.taskheet.xml`
5. Seeding only adds and updates data. Tasksheets or files that are removed are not deleted.
3. Unencrypted task files must follow this syntax: `<DATA_ID>.<FILENAME>` - For example: `39015.Festival.byaml` (The name unused, but good practice to set it appropiately)
4. Encrypted task files must follow this syntax: `<DATA_ID>.enc.<FILENAME>` - For example: `39015.enc.Festival.byaml` (The name unused, but good practice to set it appropiately)
5. Tasksheets must follow this syntax: `1.<BOSS_APP_ID>.<TASKNAME>.taskheet.xml`
6. Seeding only adds and updates data. Tasksheets or files that are removed are not deleted.

View File

@ -2,6 +2,7 @@ import path from 'path';
import fs from 'fs/promises';
import { Command } from 'commander';
import { xml2js } from 'xml-js';
import BOSS from 'boss-js';
import { getCliContext } from './utils';
import { seedFolder } from './root';
import type { CliContext } from './utils';
@ -21,7 +22,14 @@ export async function uploadFileIfChanged(ops: UploadFileOptions): Promise<void>
console.warn(`${ops.dataId}: Could not find file on disk the specified data ID - skipping`);
return;
}
const fileContents = await fs.readFile(path.join(seedFolder, 'files', newTaskFileName));
let fileContents = await fs.readFile(path.join(seedFolder, 'files', newTaskFileName));
if (newTaskFileName.startsWith(`${ops.dataId}.enc.`)) {
// File is encrypted, let's decrypt before processing
console.log(`${ops.dataId}: File is encrypted, decrypting...`);
const keys = ops.ctx.getWiiuKeys();
const decryptedContents = BOSS.decryptWiiU(fileContents, keys.aesKey, keys.hmacKey);
fileContents = decryptedContents.content;
}
const allExistingTaskFiles = await ops.ctx.grpc.listFiles({
bossAppId: ops.bossAppId,

View File

@ -2,8 +2,11 @@ import { BOSSDefinition } from '@pretendonetwork/grpc/boss/boss_service';
import { createChannel, createClient, Metadata } from 'nice-grpc';
import type { BOSSClient } from '@pretendonetwork/grpc/boss/boss_service';
export type WiiuKeys = { aesKey: string; hmacKey: string };
export type CliContext = {
grpc: BOSSClient;
getWiiuKeys: () => WiiuKeys;
};
export function getCliContext(): CliContext {
@ -27,7 +30,22 @@ export function getCliContext(): CliContext {
});
return {
grpc: client
grpc: client,
getWiiuKeys(): WiiuKeys {
const aesKey = process.env.PN_BOSS_CLI_WIIU_AES_KEY ?? '';
const hmacKey = process.env.PN_BOSS_CLI_WIIU_HMAC_KEY ?? '';
if (!aesKey) {
throw new Error('Missing env variable PN_BOSS_CLI_WIIU_AES_KEY - needed for decryption');
}
if (!hmacKey) {
throw new Error('Missing env variable PN_BOSS_CLI_WIIU_HMAC_KEY - needed for decryption');
}
return {
aesKey,
hmacKey
};
}
};
}

42
src/types/boss-js.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
declare module 'boss-js' {
export type EncryptionInput = Buffer | string;
export type EncryptionKey = Buffer | string;
export type DecryptionResultWiiu = {
hash_type: number;
iv: Buffer<ArrayBuffer>;
hmac: Buffer<ArrayBuffer>;
content: Buffer<ArrayBuffer>;
};
export type DecryptionResult3ds = {
hash_type: number;
release_date: bigint;
iv: Buffer<ArrayBuffer>;
content_header_hash: Buffer<ArrayBuffer>;
content_header_hash_signature: Buffer<ArrayBuffer>;
payload_content_header_hash: Buffer<ArrayBuffer>;
payload_content_header_hash_signature: Buffer<ArrayBuffer>;
program_id: Buffer<ArrayBuffer>;
content_datatype: number;
ns_data_id: number;
content: Buffer<ArrayBuffer>;
};
export type EncryptionOptions3ds = {
program_id?: string;
title_id?: number;
content_datatype: number;
ns_data_id: number;
};
export function encrypt(input: EncryptionInput, version: number, aesKey: string, options: EncryptionOptions3ds): Buffer<ArrayBuffer>;
export function encrypt(input: EncryptionInput, version: number, aesKey: string, hmacKey: string): Buffer<ArrayBuffer>;
export function decrypt(input: EncryptionInput, aesKey: EncryptionKey, hmacKey?: string): DecryptionResultWiiu | DecryptionResult3ds;
export function encryptWiiU(input: EncryptionInput, aesKey: string, hmacKey: string): Buffer<ArrayBuffer>;
export function decryptWiiU(input: EncryptionInput, aesKey: string, hmacKey: string): DecryptionResultWiiu;
export function encrypt3DS(input: EncryptionInput, aesKey: EncryptionKey, options: EncryptionOptions3ds): Buffer<ArrayBuffer>;
export function decrypt3DS(input: EncryptionInput, aesKey: EncryptionKey): DecryptionResult3ds;
}