diff --git a/src/cli/index.ts b/src/cli/index.ts index 8e42f2a..0491742 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -4,4 +4,5 @@ export * as splatnet2 from './splatnet2.js'; export * as nooklink from './nooklink.js'; export * as pctl from './pctl.js'; export * as androidZncaApiServerFrida from './android-znca-api-server-frida.js'; +export * as util from './util.js'; export * as app from './app.js'; diff --git a/src/cli/util.ts b/src/cli/util.ts new file mode 100644 index 0000000..0959a6c --- /dev/null +++ b/src/cli/util.ts @@ -0,0 +1,20 @@ +import createDebug from 'debug'; +import type { Arguments as ParentArguments } from '../cli.js'; +import { Argv, YargsArguments } from '../util.js'; +import * as commands from './util/index.js'; + +const debug = createDebug('cli:util'); + +export const command = 'util '; +export const desc = 'Utilities'; + +export function builder(yargs: Argv) { + for (const command of Object.values(commands)) { + // @ts-expect-error + yargs.command(command); + } + + return yargs; +} + +export type Arguments = YargsArguments>; diff --git a/src/cli/util/captureid.ts b/src/cli/util/captureid.ts new file mode 100644 index 0000000..857c386 --- /dev/null +++ b/src/cli/util/captureid.ts @@ -0,0 +1,82 @@ +import * as crypto from 'crypto'; +import createDebug from 'debug'; +import type { Arguments as ParentArguments } from '../../cli.js'; +import { Argv } from '../../util.js'; + +const debug = createDebug('cli:util:captureid'); + +export const command = 'captureid'; +export const desc = 'Encrypt/decrypt capture IDs'; + +export function builder(yargs: Argv) { + return yargs.demandCommand().command('encrypt ', 'Title ID to Capture ID', yargs => { + return yargs.positional('titleid', { + describe: 'Title ID', + type: 'string', + demandOption: true, + }); + }, argv => { + console.log(encrypt(argv.titleid)); + }).command('decrypt ', 'Capture ID to Title ID', yargs => { + return yargs.positional('captureid', { + describe: 'Capture ID', + type: 'string', + demandOption: true, + }); + }, argv => { + console.log(decrypt(argv.captureid)); +} + +const key = Buffer.from('b7ed7a66c80b4b008baf7f0589c08224', 'hex'); + +/** + * @param {string} tid Hex-encoded 8-byte title ID + * @return {string} Hex-encoded 16-byte capture ID + */ +export function encrypt(tid: string) { + if (typeof tid !== 'string' || !tid.match(/^[0-9A-Fa-f]{16}$/)) { + throw new Error('tid must be a valid title ID'); + } + + const tidb = Buffer.from('0000000000000000' + tid, 'hex').reverse(); + + const cipher = crypto.createCipheriv('aes-128-ecb', key, null); + cipher.setAutoPadding(false); + + const cidb = Buffer.concat([ + cipher.update(tidb), + cipher.final(), + ]); + + const cid = cidb.toString('hex').toUpperCase(); + + return cid; +} + +/** + * @param {string} cid Hex-encoded 16-byte capture ID + * @return {string} Hex-encoded 8-byte title ID + */ +export function decrypt(cid: string) { + if (typeof cid !== 'string' || !cid.match(/^[0-9A-Fa-f]{32}$/)) { + throw new Error('cid must be a valid capture ID'); + } + + const cidb = Buffer.from(cid, 'hex'); + + const cipher = crypto.createDecipheriv('aes-128-ecb', key, null); + cipher.setAutoPadding(false); + + const tidb = Buffer.concat([ + cipher.update(cidb), + cipher.final(), + ]).reverse(); + + if (!Buffer.alloc(8).equals(tidb.slice(0, 8))) { + throw new Error('Invalid title ID'); + } + + const tid = tidb.slice(8, 16).toString('hex'); + + return tid; +} diff --git a/src/cli/util/index.ts b/src/cli/util/index.ts new file mode 100644 index 0000000..7560de2 --- /dev/null +++ b/src/cli/util/index.ts @@ -0,0 +1 @@ +export * as captureid from './captureid.js';