/**
 * XChaCha20-Poly1305 AEAD with row-bound AAD.
 *
 * Every encrypted row binds (scope, scopeId, key, keyVersion) into the AAD.
 * Swapping a row from one slot to another (e.g. via `UPDATE secrets SET key=...`)
 * invalidates the AAD and triggers an AEAD failure on decrypt — making
 * row-substitution attacks observable.
 *
 * Wire format (base64-encoded): `nonce[24] || ciphertext || tag[16]`.
 * The nonce is randomly generated per encrypt; XChaCha20's 192-bit nonce
 * makes random selection collision-safe across realistic key lifetimes.
 */
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
import { randomBytes } from '@noble/ciphers/webcrypto';
const NONCE_LEN = 24;
const utf8 = new TextEncoder();
const utf8d = new TextDecoder();
/**
 * Canonical AAD encoding. We bind the four-tuple via JSON-array encoding
 * so the result is unambiguous regardless of what characters the
 * components carry. Earlier versions used a `|` join, which collided
 * silently if any component happened to contain the pipe character —
 * `(scope=instance, key='a|b', scopeId='c', mk_v1)` produced the same
 * AAD bytes as `(scope=instance, key='a', scopeId='b|c', mk_v1)`,
 * opening a row-swap window. JSON.stringify on a fixed-shape array is
 * trivially auditable and robust to any future key/scopeId additions
 * (e.g., connector keys with `|` or quoted scopeIds).
 *
 * Encoding choice is part of the wire format — changing it invalidates
 * every existing ciphertext. The branch this lands in has not yet
 * shipped Phase 5/6, so no rotation runbook is needed.
 */
function aad(input) {
    return utf8.encode(JSON.stringify([input.scope, input.scopeId, input.key, input.keyVersion]));
}
function toBase64(bytes) {
    // Buffer is the fastest path on Node and is already a transitive dep.
    return Buffer.from(bytes).toString('base64');
}
function fromBase64(b64) {
    return new Uint8Array(Buffer.from(b64, 'base64'));
}
function concat(a, b) {
    const out = new Uint8Array(a.length + b.length);
    out.set(a, 0);
    out.set(b, a.length);
    return out;
}
/**
 * Encrypt `plaintext` under `masterKey` with AAD bound to `ctx`.
 * Returns base64(nonce || ciphertext || tag) and the keyVersion that was
 * baked into the AAD. Caller is responsible for storing both.
 */
export function encrypt(masterKey, plaintext, ctx) {
    if (masterKey.length !== 32) {
        throw new Error(`encryption: master key must be 32 bytes (got ${masterKey.length})`);
    }
    const nonce = randomBytes(NONCE_LEN);
    const cipher = xchacha20poly1305(masterKey, nonce, aad(ctx));
    const ct = cipher.encrypt(utf8.encode(plaintext));
    return { ciphertext: toBase64(concat(nonce, ct)), keyVersion: ctx.keyVersion };
}
/**
 * Decrypt a row. Throws on:
 *   - AAD mismatch (row moved between slots; key/scope/version doesn't match)
 *   - tag failure (ciphertext or nonce tampered)
 *   - wrong master key
 *   - malformed wire format (truncated, bad base64)
 *
 * The thrown error MUST NOT carry the ciphertext or any partial plaintext.
 */
export function decrypt(masterKey, row, ctx) {
    if (masterKey.length !== 32) {
        throw new Error(`encryption: master key must be 32 bytes (got ${masterKey.length})`);
    }
    const buf = fromBase64(row.ciphertext);
    if (buf.length < NONCE_LEN + 16) {
        throw new Error('encryption: ciphertext too short');
    }
    const nonce = buf.slice(0, NONCE_LEN);
    const ct = buf.slice(NONCE_LEN);
    // The keyVersion in the AAD comes from `ctx`, not `row` — caller decides
    // which version to attempt. (rotate_master flips ctx.keyVersion to mk_v(N+1)
    // and, on failure, retries with mk_v(N) using the previous master key.)
    const cipher = xchacha20poly1305(masterKey, nonce, aad(ctx));
    const pt = cipher.decrypt(ct);
    return utf8d.decode(pt);
}
/**
 * Generate a fresh master key suitable for `encrypt()`. 32 random bytes.
 * Caller is responsible for base64-encoding for env-var transport and
 * persistence (see master-key.ts).
 */
export function generateMasterKey() {
    return randomBytes(32);
}
//# sourceMappingURL=encryption.js.map