import { BasaltError } from './errors.js'; /** * The shared AEAD primitive behind every Basalt "secret at rest" box * (`@basaltkit/auth`'s `SecretBox` for TOTP secrets, `@basaltkit/drives`' * `DriveSecretBox` for OAuth tokens). Exported from the * `@basaltkit/core/secret-box` subpath only, never from the main barrel. * * ## Envelope * * .... * * iv, tag and ciphertext base64url. AES-256-GCM with a 96-bit random IV and * the full 128-bit tag. Each box key is derived from its key material with * HKDF-SHA256 under a per-box `info` label, so the same material used by two * boxes yields two unrelated keys. * * ## Associated data * * NUL NUL NUL … NUL * * The context binds a ciphertext to its record (a user, a tenant + connection): * a blob copied into another row fails the tag check instead of decrypting * there. The key id is covered too, so it cannot be swapped. NUL is refused * inside a field, which keeps the encoding unambiguous. * * There is no plaintext path: anything that is not an envelope of this box's * version is refused. Legacy formats are the wrapper's business. */ /** Why a {@link SecretBoxError} was raised — wrappers map it to their own errors. */ export type SecretBoxFailure = /** The box configuration (keys, version, info, field count) is unusable. */ 'config' /** Not an envelope of this box's version, or structurally malformed. */ | 'malformed' /** The envelope names a key id the ring does not hold. */ | 'unknown-key' /** The context has the wrong number of fields, or a field contains NUL. */ | 'context' /** The tag did not verify: wrong key, tampering, or another record's blob. */ | 'auth-failed'; export declare class SecretBoxError extends BasaltError { readonly failure: SecretBoxFailure; readonly detail: string; /** For `unknown-key`: the key id the envelope named. */ readonly keyId?: string | undefined; readonly status = 500; readonly expose = false; constructor(failure: SecretBoxFailure, detail: string, /** For `unknown-key`: the key id the envelope named. */ keyId?: string | undefined); } /** One entry of the key ring. */ export interface SecretBoxKey { /** * Stable identifier stored with every ciphertext (`[A-Za-z0-9_-]{1,64}`). * Keep it in the ring as long as any row was sealed with it. */ id: string; /** Key material, at least 32 bytes. Never used raw: the AES key is derived with HKDF. */ key: string | Uint8Array; } export interface CreateSecretBoxOptions { /** The key ring; the first key seals everything new, every key opens. */ keys: readonly SecretBoxKey[]; /** HKDF `info` label, unique per box (e.g. `'basalt:auth:secret-box:v2'`). */ info: string; /** Envelope version tag, `[a-z0-9]{1,16}` (e.g. `'bka2'`). */ version: string; /** How many context fields every seal/open must pass. */ aadFields: number; } export interface SecretBoxPrimitive { /** The envelope version this box writes and reads. */ readonly version: string; /** The key id new ciphertexts are sealed with. */ readonly activeKeyId: string; /** Encrypts `plaintext` under the active key, bound to `context`. */ seal(plaintext: string, context: readonly string[]): string; /** Decrypts an envelope sealed for `context`; throws {@link SecretBoxError} otherwise. */ open(envelope: string, context: readonly string[]): string; /** Whether `envelope` has this box's shape and the active key id (no decryption). */ isCurrent(envelope: string): boolean; /** The key id an envelope of this box names, or null when it is not one. */ keyIdOf(envelope: string): string | null; /** * Re-seals `envelope` under the active key. Returns null when it is already * current — after authenticating it, so a blob that does not open is never * vouched for. */ reseal(envelope: string, context: readonly string[]): string | null; } /** Builds an AES-256-GCM box over a key ring. See the module comment. */ export declare function createSecretBox(options: CreateSecretBoxOptions): SecretBoxPrimitive;