Secret handling discipline
Related:
- 004-single-passphrase.md
- 018-systemd-lifecycle.md
cli/src/secret.rs
Context
braid handles two kinds of LUKS secret material in process memory:
- user-entered passphrases used for
cryptsetupopen, verify, format, and keyfile enrollment; - generated keyfile bytes used for slot-1 auto-unlock enrollment.
These values must exist briefly in process memory, but they should not escape that narrow window through ordinary Rust strings, buffered readers, command arguments, debug output, or long-lived scopes.
Decision
LUKS passphrase plaintext is represented by secret::Passphrase, a newtype
around Zeroizing<String>. LUKS keyfile byte buffers remain
Zeroizing<[u8; KEYFILE_SIZE]> because the generated bytes never leave the
function frame that writes them.
Every passphrase read path must use unbuffered Read, not BufRead, and must
consume input one byte at a time into pre-sized zeroizing storage. This avoids
std-internal buffering that can retain plaintext outside braid-owned
Zeroizing values. Confirmation reads in cli/src/confirm.rs intentionally
accept Read, not BufRead, for the same reason: confirmation must not
pre-drain bytes needed by a later --passphrase-stdin read.
Every secret-bearing read must enforce a hard byte cap while reading.
Passphrase reads use PASSPHRASE_MAX_BYTES = 64 * 1024; confirmation reads
use CONFIRM_MAX_BYTES = 256. New secret-read sites must declare and enforce
their own cap instead of allowing unbounded growth of a zeroizing buffer.
Anything inside a Passphrase must reach subprocesses through
CommandRunner::run_with_stdin, never through CmdRequest::to_argv. The
Passphrase::expose_secret() method is the grep-friendly plaintext egress
point for these handoffs. ps(1) must never be able to surface a passphrase.
Generated random secrets must drop before any later syscall whose duration is
unbounded. In particular, generated keyfile bytes are scoped so the
Zeroizing<[u8; KEYFILE_SIZE]> is dropped before the durability
sync_all() on the written file.
Every type that owns secret bytes must implement Debug with redacted output.
The canonical rendering is <redacted>.
braid does not use in-process passphrase equality as an authentication mechanism. Normal passphrase verification is delegated to cryptsetup/LUKS. The only current in-process comparison is the local double-prompt confirmation flow for fresh formatting, where braid checks that two user-entered strings match before one becomes the new pool passphrase.
Threat Model
These rules harden braid’s in-process memory image against accidental plaintext
retention in process snapshots, core dumps, and swap residue. They do not
defend against a privileged attacker on the running host with ptrace,
/dev/mem, root access to /proc/<pid>/mem, or equivalent capabilities.
The target invariant is narrower: no plaintext beyond the smallest practical in-process window, and no untyped plaintext values at module boundaries.