From dc471eefa92e20ed1b683d72ad99428c55ac850d Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 14 Sep 2025 19:22:46 +0200 Subject: [PATCH] feat: Add support for importing encrypted files --- seeding/README.md | 7 ++++--- src/cli/seed.cmd.ts | 10 +++++++++- src/cli/utils.ts | 20 +++++++++++++++++++- src/types/boss-js.d.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 src/types/boss-js.d.ts diff --git a/seeding/README.md b/seeding/README.md index 5d47b77..60a81e7 100644 --- a/seeding/README.md +++ b/seeding/README.md @@ -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...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: `.` - For example: `39015.Festival.byaml` (The name unused, but good practice to set it appropiately) +4. Encrypted task files must follow this syntax: `.enc.` - For example: `39015.enc.Festival.byaml` (The name unused, but good practice to set it appropiately) +5. Tasksheets must follow this syntax: `1...taskheet.xml` +6. Seeding only adds and updates data. Tasksheets or files that are removed are not deleted. diff --git a/src/cli/seed.cmd.ts b/src/cli/seed.cmd.ts index e2e30cf..81c81ef 100644 --- a/src/cli/seed.cmd.ts +++ b/src/cli/seed.cmd.ts @@ -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 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, diff --git a/src/cli/utils.ts b/src/cli/utils.ts index d5b0609..fbefbd1 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -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 + }; + } }; } diff --git a/src/types/boss-js.d.ts b/src/types/boss-js.d.ts new file mode 100644 index 0000000..50a45eb --- /dev/null +++ b/src/types/boss-js.d.ts @@ -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; + hmac: Buffer; + content: Buffer; + }; + + export type DecryptionResult3ds = { + hash_type: number; + release_date: bigint; + iv: Buffer; + content_header_hash: Buffer; + content_header_hash_signature: Buffer; + payload_content_header_hash: Buffer; + payload_content_header_hash_signature: Buffer; + program_id: Buffer; + content_datatype: number; + ns_data_id: number; + content: Buffer; + }; + + 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; + export function encrypt(input: EncryptionInput, version: number, aesKey: string, hmacKey: string): Buffer; + export function decrypt(input: EncryptionInput, aesKey: EncryptionKey, hmacKey?: string): DecryptionResultWiiu | DecryptionResult3ds; + + export function encryptWiiU(input: EncryptionInput, aesKey: string, hmacKey: string): Buffer; + export function decryptWiiU(input: EncryptionInput, aesKey: string, hmacKey: string): DecryptionResultWiiu; + + export function encrypt3DS(input: EncryptionInput, aesKey: EncryptionKey, options: EncryptionOptions3ds): Buffer; + export function decrypt3DS(input: EncryptionInput, aesKey: EncryptionKey): DecryptionResult3ds; +}