/** * Secret Generation and Derivation * * Generates cryptographically secure secrets in various formats * and derives related secrets (e.g., WireGuard public keys from private keys) */ import { execFileSync, execSync } from 'node:child_process'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; export interface SecretGenerationOptions { format: string; length?: number; } /** * Mint a passphraseless GPG/PGP signing key for `identity` and return the * base64'd ASCII-armored PRIVATE key — the shape consuming Ansible roles decode * (e.g. celilo-apt-repo's reprepro signing key). For `generate: {method: gpg}` * secrets: celilo owns the signing key as infrastructure, not the operator. * * The key is generated in a throwaway GNUPGHOME (never touches the host * operator's keyring) and is passphraseless because celilo encrypts it at rest * in the secret store. Requires `gnupg` on the celilo host. Uses execFileSync * (argv, no shell) so `identity` can't inject. */ export function generateGpgPrivateKey(identity: string): string { const home = mkdtempSync(join(tmpdir(), 'celilo-gpg-')); const env = { ...process.env, GNUPGHOME: home }; try { execFileSync( 'gpg', [ '--batch', '--pinentry-mode', 'loopback', '--passphrase', '', '--quick-generate-key', identity, 'default', 'sign', 'never', ], { env, stdio: 'pipe' }, ); const armor = execFileSync( 'gpg', [ '--batch', '--pinentry-mode', 'loopback', '--passphrase', '', '--armor', '--export-secret-keys', ], { env, encoding: 'utf-8' }, ); if (!armor.includes('BEGIN PGP PRIVATE KEY')) { throw new Error('gpg produced no private-key armor'); } return Buffer.from(armor, 'utf-8').toString('base64'); } catch (err) { throw new Error( `Failed to generate GPG signing key for "${identity}": ${ err instanceof Error ? err.message : String(err) }. Is gnupg installed on the celilo host?`, ); } finally { rmSync(home, { recursive: true, force: true }); } } export interface SecretDerivationOptions { sourceSecret: string; deriveMethod: string; } /** * Generate cryptographically secure secret * * Supported formats: * - base64: Random bytes, base64 encoded (default) * - hex: Random bytes, hex encoded * - wireguard-key: 32 random bytes, base64 (WireGuard format) * - tsig-key: HMAC-SHA256 key (32 bytes, base64) * * @param options - Generation options (format, length) * @returns Generated secret string */ export function generateSecret(options: SecretGenerationOptions): string { const { format = 'base64', length = 32 } = options; // Generate random bytes const bytes = new Uint8Array(length); crypto.getRandomValues(bytes); switch (format) { case 'base64': case 'wireguard-key': case 'tsig-key': // All these formats use base64 encoding return btoa(String.fromCharCode(...bytes)); case 'hex': // Hex encoding return Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) .join(''); default: throw new Error( `Unknown secret format: ${format}\nValid formats: base64, hex, wireguard-key, tsig-key`, ); } } /** * Derive secret from source secret * * Supported derivation methods: * - wireguard-pubkey: Derive WireGuard public key from private key * * @param options - Derivation options (sourceSecret, deriveMethod) * @returns Derived secret string */ export function deriveSecret(options: SecretDerivationOptions): string { const { sourceSecret, deriveMethod } = options; switch (deriveMethod) { case 'wireguard-pubkey': return deriveWireguardPublicKey(sourceSecret); default: throw new Error( `Unknown derivation method: ${deriveMethod}\nValid methods: wireguard-pubkey`, ); } } /** * Derive WireGuard public key from private key * * Uses system `wg pubkey` command (requires WireGuard tools installed) * * @param privateKey - Base64-encoded WireGuard private key * @returns Base64-encoded WireGuard public key */ function deriveWireguardPublicKey(privateKey: string): string { try { // Use wg pubkey command to derive public key const publicKey = execSync('wg pubkey', { input: privateKey, encoding: 'utf-8', }).trim(); return publicKey; } catch (error) { // Check if wg command not found if (error instanceof Error && error.message.includes('not found')) { throw new Error( 'Failed to derive WireGuard public key.\n\n' + 'WireGuard tools not found. Install with:\n' + ' macOS: brew install wireguard-tools\n' + ' Ubuntu/Debian: apt-get install wireguard-tools\n' + ' Arch: pacman -S wireguard-tools', ); } throw new Error( `Failed to derive WireGuard public key: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Check if a secret format requires derivation * * @param format - Secret format * @returns True if format should be derived, not generated */ export function isDerivableFormat(_format: string): boolean { // Currently no formats are auto-derived by format alone // Derivation is controlled by derive_from in schema return false; }