Vault

Sealed keystore that anchors every other framework subsystem holding secrets at rest: db field encryption, encrypted session storage, audit-log signing keys, OAuth refresh tokens, anything that flows through b.vault.seal / b.vault.unseal. The vault is the single trust root for the framework — rotate it and everything sealed under the old keys re-seals as part of the same operation.

Keys held: an ML-KEM-1024 + ECDH P-384 hybrid keypair plus a per-deployment derivedHash salt. After init() the keypair never leaves the process in any decrypted form except via the seal / unseal API.

Modes (wrapped is the default; plaintext is opt-out with an explicit boot warning per the framework's modernity stance):

- wrappedvault.key.sealed file, passphrase-derived AEAD wrap (Argon2id → SHAKE256 → XChaCha20-Poly1305). The plaintext keypair never lands on disk. - plaintextvault.key JSON at mode 0o600. Development only. Emits a console.warn at every boot.

Two-API contract: bootstrap awaits init() once, and every other consumer (often at module-require time across hundreds of call sites) runs synchronously against the in-process key cache.

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
var sealed = b.vault.seal("4111-1111-1111-1111");
sealed.startsWith("vault:");      // → true
b.vault.unseal(sealed);           // → "4111-1111-1111-1111"

Rotating the KEK (passphrase change, sealed-blob refresh, hardware-token swap) is a separate primitive — b.vaultRotate.rotate walks every sealed column under the old keypair and re-seals it under the new one with batched commits and a round-trip verify. The vault module owns the in-process cache; the rotator owns the on-disk data sweep.

// Wrapped-mode bootstrap (first run): the vault generates an
// ML-KEM-1024 + P-384 keypair, wraps it under the operator's
// passphrase, and writes vault.key.sealed atomically.
process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
await b.vault.init({ dataDir: "/var/lib/blamejs" });
b.vault.getMode();                // → "wrapped"

Sealed-value format: "vault:" prefix + base64 envelope produced by b.crypto.encrypt. Old envelopes always remain readable (envelope versioning); new writes use whichever KEM / CIPHER / KDF the active framework version pins as default.

b.vault.getDerivedHashSalt() #

0.8.42

Returns the 32-byte per-deployment salt used by crypto-field's derivedHash columns. The salt is generated once on first init, persisted at vault.derived-hash-salt (mode 0o600) inside dataDir, and read back on subsequent boots. It survives vault KEK rotations — different file from vault.key.sealed — so indexed-lookup determinism for derivedHash columns holds across a passphrase change.

Why per-deployment: pre-v0.8.42 the deterministic sha3(namespace + plaintext) shape allowed cross-deployment rainbow tables and cross-table correlation between deployments sharing a namespace. Binding a 32-byte salt closes that class without losing the determinism inside a single deployment that makes the index lookup possible.

Throws VaultError("vault/not-initialized") if init() has not been awaited yet. Throws vault/derived-hash-salt-corrupted if the on-disk file exists but is not exactly 32 bytes.

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
var salt = b.vault.getDerivedHashSalt();
salt.length;          // → 32
Buffer.isBuffer(salt); // → true

// Same value on every call within a process — cached.
b.vault.getDerivedHashSalt() === salt;  // → true

b.vault.getDerivedHashMacKey() #

0.14.7

Returns the 32-byte per-deployment SECRET key that backs crypto- field's keyed (hmac-shake256) derived-hash mode. Generated once on first use, SEALED at rest (vault.derived-hash-mac.sealed, mode 0o600) so disk access alone does not expose it, and re-sealed by an envelope vault rotation. Distinct from getDerivedHashSalt, which is a non-secret salt stored in plaintext.

Throws VaultError("vault/not-initialized") before init(), or vault/derived-hash-mac-key-corrupted if the sealed file does not unseal to exactly 32 bytes.

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
var k = b.vault.getDerivedHashMacKey();
k.length;           // → 32
Buffer.isBuffer(k); // → true

b.vault.init(opts) #

0.1.0
{
  {
    dataDir: string,    // required — directory holding vault.key /
                        //            vault.key.sealed / derived-hash-salt
    mode:    string,    // "wrapped" (default) | "plaintext"
  }
}

Bootstraps the vault. Call once at application startup before any code path that reads sealed values from the database, opens the encrypted session store, or signs audit-log entries. Subsequent calls after a successful init are no-ops, so guard-rail wrappers that re-call init() from worker entry points are safe.

Mode dispatch:

- wrapped (default) — if vault.key.sealed exists, prompts for the passphrase via b.vaultPassphraseSource and unwraps. If neither sealed nor plaintext file is present, generates a fresh keypair and wraps it under a freshly-prompted passphrase. - plaintext — reads vault.key if present, generates a fresh keypair and writes it at mode 0o600 otherwise. Logs a WARNING line at every boot.

Refuses to guess when both vault.key and vault.key.sealed exist in dataDir, or when the requested mode mismatches the on-disk shape (sealed file present but mode: "plaintext" requested, or vice versa). Throws a VaultError in either case so the bootstrap exits cleanly instead of silently picking one.

// Wrapped-mode bootstrap with passphrase from the env var
// b.vaultPassphraseSource consults by default.
process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
await b.vault.init({
  dataDir: "/var/lib/blamejs",
  mode:    "wrapped",
});
b.vault.getMode();   // → "wrapped"

// Re-calling init() after a successful boot is a no-op.
await b.vault.init({ dataDir: "/var/lib/blamejs" });
b.vault.getMode();   // → "wrapped"

b.vault.seal(plaintext) #

0.1.0

Synchronously encrypts plaintext under the in-process keypair and returns a "vault:"-prefixed string suitable for storage in any column declared sealed in the field-crypto schema. Called from hundreds of call sites across a typical application — keep it sync.

Idempotent on already-sealed input: a value that already starts with the vault prefix is returned unchanged so seal-on-write paths survive code that re-seals the same row twice. Empty / falsy input passes through verbatim — there's nothing to encrypt and the caller likely meant null to land in the column.

Throws VaultError("vault/not-initialized") if init() has not been awaited yet — the seal/unseal API is sync, but the keypair cache it consults is populated by the async init.

Sealed values from this primitive decrypt regardless of which row / column / table they came from. Use b.vault.aad.seal for AEAD-bound seals when copy-paste between rows is part of the threat model.

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
var sealed = b.vault.seal("4111-1111-1111-1111");
sealed.indexOf("vault:");        // → 0

// Idempotent: re-sealing returns the input unchanged.
b.vault.seal(sealed) === sealed; // → true

// Falsy input is passed through verbatim.
b.vault.seal("") === "";         // → true

b.vault.unseal(value) #

0.1.0

Synchronously decrypts a "vault:"-prefixed string produced by b.vault.seal and returns the plaintext. Idempotent on non-sealed input: a value that does not start with the vault prefix is returned unchanged so read paths that select a column before knowing whether it's sealed don't have to branch.

The envelope inside the prefix is versioned — values sealed under older KEM / KDF / cipher choices remain readable across framework upgrades. New seals always use the active algorithm set, so a full read-write cycle migrates a row forward.

Throws VaultError("vault/not-initialized") if init() has not been awaited yet. Throws on AEAD-tag failure (corrupted ciphertext, wrong keypair) — operators rotating keys validate the rotation via b.vaultRotate.verify rather than catching here.

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
var sealed = b.vault.seal("hello");
b.vault.unseal(sealed);          // → "hello"

// Non-sealed input passes through unchanged.
b.vault.unseal("plain-string");  // → "plain-string"
b.vault.unseal(null);            // → null

b.vault.getKeysJson() #

0.6.0

Returns the in-process keypair as a pretty-printed JSON string — the same shape that lives on disk for mode: "plaintext" and inside the wrapped envelope for mode: "wrapped". Used by the rotation pipeline to feed oldKeys into a fresh b.vaultRotate.rotate({ oldKeys, newKeys, ... }) call without round-tripping through disk.

The returned JSON has four properties: publicKey, privateKey (ML-KEM-1024), ecPublicKey, ecPrivateKey (P-384). Operators routing this through structured logging or telemetry must redact — these are the production keys, not metadata.

Throws VaultError("vault/not-initialized") if init() has not been awaited yet.

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
var json = b.vault.getKeysJson();
var keys = JSON.parse(json);
Object.keys(keys).sort().join(",");
// → "ecPrivateKey,ecPublicKey,privateKey,publicKey"

b.vault.getCurrentPassphrase() #

0.6.0

Returns the Buffer holding the passphrase the vault was unsealed with on this boot, or null for mode: "plaintext" and for any future scenario where the vault was bootstrapped without one. Used by passphrase-rotation flows that re-wrap the keypair under a fresh passphrase without prompting the operator twice.

The Buffer is already in the JS heap during unwrap; retaining it does not change the threat model meaningfully and is what makes b.vaultPassphraseOps.changePassphrase ergonomic. Operators concerned about heap residency rotate the passphrase and let the old Buffer get zeroed and replaced.

process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
var pass = b.vault.getCurrentPassphrase();
Buffer.isBuffer(pass);                     // → true
pass.toString("utf8");                     // → "S0meStrongPassphr@se!"

// Plaintext mode never holds a passphrase.
await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
b.vault.getCurrentPassphrase();            // → null

b.vault.getMode() #

0.6.0

Returns the active vault mode: "wrapped", "plaintext", or null before init() has been awaited. Useful from health-check endpoints that surface a deployment-posture badge ("plaintext mode — DEV ONLY") or refuse to start the public listener until the vault is in wrapped mode in production.

b.vault.getMode();                           // → null  (pre-init)

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
b.vault.getMode();                           // → "wrapped"

if (process.env.NODE_ENV === "production" && b.vault.getMode() !== "wrapped") {
  throw new Error("refusing to start: vault must be in wrapped mode");
}

b.vault.sealPemFile(opts) #

0.8.42
{
  {
    source:         string,    // plaintext PEM path (required)
    destination:    string,    // sealed-output path (required, must differ from source)
    audit:          boolean,   // emit b.audit events on every reseal (default true)
    pollInterval:   number,    // fs.watchFile cadence in ms (default 500)
    onResealed:     function,  // (info) => void — { srcPath, destPath, bytes, resealedAt, generation }
    onError:        function,  // (err)  => void — sealing failed
    maxSourceBytes: number,    // refuse source larger than this (default 1 MiB)
  }
}

Watches a plaintext PEM file (typically certbot's /etc/letsencrypt/live//privkey.pem after an ACME renewal) and re-seals it to a destination path under the vault keypair on every mtime / size change. Closes the renewal-window gap where a fresh PEM lives unencrypted on disk between certbot's write and the next operator-driven re-seal.

Crash-safe write protocol: write .tmp at mode 0o600, fsync, create a .rewriting marker, atomic rename, fsync the destination directory, remove the marker. If the framework crashes between marker create and marker remove, the next sealPemFile() start re-seals from source idempotently.

Refuses to seal in place (source === destination), refuses to follow a symlinked source (TOCTOU defense), and refuses when the destination's parent directory is group- or other-writable on POSIX. Source size is capped (maxSourceBytes, default 1 MiB) so an attacker with write access to source can't OOM the host with a 10 GiB file.

Returns a watcher handle: start (auto-called by the constructor unless overridden), stop, forceReseal({ actorId, reason }), plus read-only generation / lastResealedAt / lastError / watching properties.

await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });

var watcher = b.vault.sealPemFile({
  source:       "/etc/letsencrypt/live/example.com/privkey.pem",
  destination:  "/var/lib/blamejs/server.key.sealed",
  pollInterval: b.constants.TIME.seconds(2),
  onResealed:   function (info) {
    console.log("resealed", info.bytes, "bytes, gen", info.generation);
  },
  onError:      function (err) {
    console.error("reseal failed:", err.message);
  },
});

watcher.generation;        // → 1   (initial seal completed)
typeof watcher.lastResealedAt; // → "number"

// Force a reseal after a manual ACME renewal — captured in audit.
watcher.forceReseal({ actorId: "ops-bot", reason: "manual-renewal" });

// Stop watching at shutdown.
watcher.stop();

Last updated 2026-08-08T16:39:49.652Z by seeder.