Vault Passphrase Ops
Moving the vault key between its plaintext and sealed forms, and changing the passphrase that seals it. These are the operations where a crash at the wrong moment could leave a deployment with no readable key, so each one writes to a temporary file, verifies by unsealing what it just wrote, and only then replaces the live file.
Nothing overwrites. Sealing refuses when a sealed file already exists, unsealing refuses when a plaintext key is already there, and both refuse when a temporary file from an earlier crash is still present, because that file is evidence the directory needs looking at before another write.
Each operation has a preflight that answers the same question without doing anything, so an operator or a CLI can report what would happen before asking for it. The failure reason is the same text either way.
b.vaultPassphraseOps.preflightSealable(opts) #
{
dataDir: string, // required; the framework data directory
}
Answer { ok, reason } for whether sealing would proceed, without writing anything.
It is not ok when there is no plaintext key to seal, when a sealed file already exists, or when a .tmp file from an earlier crash is still there. seal runs this and raises vault-passphrase/preflight-failed with the same reason, so a CLI can report the refusal before asking an operator for a passphrase.
A missing dataDir, or one that does not exist, raises vault-passphrase/no-datadir: that is a question about the call rather than about the directory's state, so it raises instead of answering ok: false.
var b = require("@blamejs/core");
b.vaultPassphraseOps.preflightSealable({ dataDir: "./data" });
// → { ok: true } or { ok: false, reason: "…" }
b.vaultPassphraseOps.preflightUnsealable(opts) #
{
dataDir: string, // required; the framework data directory
}
Answer { ok, reason } for whether unsealing would proceed, without writing anything.
It is not ok when there is no sealed file, when a plaintext key is already present, or when a .tmp file from an earlier crash is still there. Refusing while a plaintext key exists is the important one: unsealing over it would replace a key that may be the only copy of what the data was encrypted under.
var b = require("@blamejs/core");
b.vaultPassphraseOps.preflightUnsealable({ dataDir: "./data" });
b.vaultPassphraseOps.preflightRotatable(opts) #
{
dataDir: string, // required; the framework data directory
}
Answer { ok, reason } for whether a passphrase rotation would proceed, without writing anything.
It is not ok when there is no sealed file, since a rotation has nothing to re-seal, or when a .tmp file from an earlier crash is still present. There is no plaintext check here: a rotation reads the sealed file and writes the sealed file, so a plaintext key beside it is not in the way.
var b = require("@blamejs/core");
b.vaultPassphraseOps.preflightRotatable({ dataDir: "./data" });
b.vaultPassphraseOps.seal(opts) #
{
dataDir: string, // required; the framework data directory
passphrase: string, // required; a Buffer of passphrase bytes
keepPlaintext: boolean, // true leaves the plaintext key on disk
}
Seal the plaintext vault key under a passphrase, so the deployment moves from a key readable on disk to one that needs the passphrase to open.
passphrase has to be a Buffer rather than a string, because a Buffer can be zeroed once the derivation is done. A string cannot, so it is refused with vault-passphrase/no-passphrase.
A missing dataDir, or one that does not exist, raises vault-passphrase/no-datadir, and a state the preflight refuses raises vault-passphrase/preflight-failed carrying its reason.
The sealed file is written to a temporary name, unsealed again and compared byte for byte with the original, and only then put in place. A round-trip that cannot be unsealed at all raises vault-passphrase/verify-failed, and one that unseals to different bytes raises vault-passphrase/verify-mismatch. Either way the temporary file is removed and the plaintext key is left exactly as it was, which both messages state.
It resolves with { sealedPath, plaintextDeleted }. The plaintext key is removed once the sealed file is in place, which is the point of sealing; keepPlaintext: true leaves it on disk and reports plaintextDeleted: false. That is for a migration that needs both forms briefly, and it means the key is still readable to anything that can read the directory, so the plaintext copy wants removing once the sealed one is proven.
keepPlaintext must be a boolean, raising vault-passphrase/bad-opt otherwise. Anything truthy was read as true, so a configuration file deserialized into the string "false" left the plaintext key on disk while the call reported it deleted.
// requires: a passphrase source, and ./data holding a plaintext vault key
var b = require("@blamejs/core");
var pass = await b.vaultPassphraseSource.getPassphrase();
try { await b.vaultPassphraseOps.seal({ dataDir: "./data", passphrase: pass }); }
finally { pass.fill(0); }
b.vaultPassphraseOps.unseal(opts) #
{
dataDir: string, // required; the framework data directory
passphrase: string, // required; a Buffer of passphrase bytes
}
Write the vault key back out in plaintext, for a deployment moving to a model where the key is protected by the filesystem rather than a passphrase.
It refuses when a plaintext key already exists, since overwriting it could destroy the only copy of the key the data was encrypted under. It also refuses while a .tmp file from an earlier crash is present.
The plaintext is written to a temporary name and verified first, and only then put in place. **The sealed file is deleted once it is.** An interrupted unseal therefore leaves the sealed file intact and the deployment booting as before, but a completed one does not: the directory now holds a plaintext key and no sealed one.
That is a mode change, not just a file change. b.vault.init refuses wrapped mode when only the plaintext key is present, so the deployment has to boot as plaintext afterwards, or be re-sealed with b.vaultPassphraseOps.seal before it boots again.
It resolves with { plaintextPath }.
// requires: a passphrase source, and ./data holding a sealed vault key
var b = require("@blamejs/core");
var pass = await b.vaultPassphraseSource.getPassphrase();
try {
await b.vaultPassphraseOps.unseal({ dataDir: "./data", passphrase: pass });
} finally {
pass.fill(0);
}
// ./data now holds the plaintext key and no sealed one: boot as
// mode "plaintext", or re-seal before starting again.
b.vaultPassphraseOps.rotate(opts) #
{
dataDir: string, // required; the framework data directory
oldPassphrase: string, // required; a Buffer that opens the current file
newPassphrase: string, // required; a Buffer to re-seal under
}
Change the passphrase a sealed vault key is sealed under. The keypair itself does not change, so nothing encrypted under it needs re-encrypting: this is the operation for a passphrase that leaked or for an operator leaving, not for retiring a key.
Both oldPassphrase and newPassphrase are required Buffers. The old one has to open the existing file, so a rotation cannot be used to replace a sealed key the caller cannot already read.
The re-sealed file is written to a temporary name, unsealed under the new passphrase and compared with the original bytes before replacing anything. A failure leaves the existing sealed file untouched, which the error message states, because the operator's next question is whether the vault still opens with the old passphrase.
Retiring the keypair rather than the passphrase is b.vaultRotate.rotate.
// requires: OLD_PASSPHRASE and NEW_PASSPHRASE set, and ./data holding a
// sealed vault key the old one opens
var b = require("@blamejs/core");
await b.vaultPassphraseOps.rotate({
dataDir: "./data",
oldPassphrase: Buffer.from(process.env.OLD_PASSPHRASE),
newPassphrase: Buffer.from(process.env.NEW_PASSPHRASE),
});