/** * Internal helpers shared by the public {@link createRootKeys} / * {@link createServerKeys} / {@link openVault} surface. These are NOT part of * the package's public API — they are an implementation seam that keeps * `orchestration.ts` under the file-size budget. Consumers always go through * the top-level barrel. */ import { aeadDecrypt, aeadEncrypt } from "@suaveplan/crypto"; import { encodePublicKeychain, extractPublicKeys, type Keychain, type PublicKeychain, } from "@suaveplan/crypto/keychain"; import { ed25519 } from "@suaveplan/crypto-curve"; import { DEFAULT_ARGON2ID_PARAMS, deriveKeyWithArgon2id, } from "../argon2id/argon2id.js"; import { CRYPTO_VAULT_CODES } from "../errors/codes.js"; import { CryptoVaultError } from "../errors/crypto-vault-error.js"; import type { Argon2idParams } from "../types/types.js"; import type { LayoutV1Fields } from "./orchestration.types.js"; /** Length of the Argon2id salt embedded in the vault header. */ export const SALT_BYTES = 16; /** Length of the XChaCha20-Poly1305 nonce embedded in the vault header. */ export const NONCE_BYTES = 24; /** Length of an Ed25519 signature appended to the public-key file. */ export const SIGNATURE_BYTES = 64; /** Combined header size: salt (16) + nonce (24). */ export const VAULT_HEADER_BYTES = SALT_BYTES + NONCE_BYTES; const PASSWORD_ENCODER = new TextEncoder(); function randomBytes(length: number): Uint8Array { const out = new Uint8Array(length); crypto.getRandomValues(out); return out; } /** * Encrypt `plaintext` under a password-derived KEK. Layout: * `salt(16) || nonce(24) || ciphertext`. Both header values are drawn fresh * so two encryptions of the same plaintext under the same password share no * bytes. */ export async function sealVault( plaintext: Uint8Array, password: string, params: Argon2idParams ): Promise { const salt = randomBytes(SALT_BYTES); const nonce = randomBytes(NONCE_BYTES); const kek = await deriveKeyWithArgon2id( PASSWORD_ENCODER.encode(password), salt, params ); const ciphertext = aeadEncrypt("XChaCha20-Poly1305", kek, plaintext, { nonce, }); const out = new Uint8Array(VAULT_HEADER_BYTES + ciphertext.length); out.set(salt, 0); out.set(nonce, SALT_BYTES); out.set(ciphertext, VAULT_HEADER_BYTES); return out; } /** * Inverse of {@link sealVault}. Any failure — short input, wrong password, or * tampered ciphertext — surfaces as a single * `CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.DECRYPT_FAILED)` so callers do * not learn whether the password or the integrity tag was the failure. */ export async function openVaultBytes( vaultBytes: Uint8Array, password: string ): Promise { if (vaultBytes.length <= VAULT_HEADER_BYTES) { throw new CryptoVaultError( CRYPTO_VAULT_CODES["ORCHESTRATION.DECRYPT_FAILED"], "error.crypto-vault.orchestration.decrypt-failed", { context: { reason: "short-input", length: vaultBytes.length } } ); } const salt = vaultBytes.slice(0, SALT_BYTES); const nonce = vaultBytes.slice(SALT_BYTES, VAULT_HEADER_BYTES); const ciphertext = vaultBytes.slice(VAULT_HEADER_BYTES); const kek = await deriveKeyWithArgon2id( PASSWORD_ENCODER.encode(password), salt, DEFAULT_ARGON2ID_PARAMS ); try { return aeadDecrypt("XChaCha20-Poly1305", kek, ciphertext, { nonce }); } catch { throw new CryptoVaultError( CRYPTO_VAULT_CODES["ORCHESTRATION.DECRYPT_FAILED"], "error.crypto-vault.orchestration.decrypt-failed", { context: { reason: "aead-verify-failed" } } ); } } /** * Sign `data` with the supplied Ed25519 secret key and append the 64-byte * signature, producing the canonical signed public-key file layout * `data || signature`. */ function signAndAppend( data: Uint8Array, ed25519SecretKey: Uint8Array ): Uint8Array { const signature = ed25519.sign(data, ed25519SecretKey); const out = new Uint8Array(data.length + signature.length); out.set(data, 0); out.set(signature, data.length); return out; } /** * Project `keychain` to its public-only counterpart, encode, sign, and * package both forms for callers. */ export function buildPublicKeyFile( keychain: Keychain, signingSlotSecretKey: Uint8Array ): { publicKeyBytes: Uint8Array; publicKeys: PublicKeychain; } { const publicKeys = extractPublicKeys(keychain); const encoded = encodePublicKeychain(publicKeys); return { publicKeyBytes: signAndAppend(encoded, signingSlotSecretKey), publicKeys, }; }