Documentation
¶
Overview ¶
Package backupcrypt is the D1.4 backup-envelope cryptography leaf: optional AES-256-GCM encryption of the compressed tar.gz backup blob, with the fixed 43-byte header bound to the ciphertext via AAD so any header tampering — including a KDF iteration-count downgrade — fails authentication. It is a self-contained leaf (stdlib + golang.org/x/crypto/pbkdf2 only) extracted from package main per ADR-0002.
On-disk format:
offset size field notes ------ ---- ------------- --------------------------------- 0 8 magic "CVRTBK01" (ASCII) 8 1 version 0x01 9 1 kdf_id 0x01 = PBKDF2-SHA256 10 4 kdf_iters uint32 BE; 600000 for v1 14 16 salt random 30 1 cipher_id 0x01 = AES-256-GCM 31 12 nonce random 43 .. ciphertext compressed tar.gz, AES-GCM-sealed end-16 16 tag GCM auth tag (last 16 bytes)
Restore detection: the magic bytes "CV" (0x43 0x56) cannot collide with gzip's magic (0x1F 0x8B), so the restore reader can sniff the first eight bytes to choose between the encrypted path and the existing D1.3a unencrypted tar.gz path.
Wrong-passphrase vs. tamper: AES-GCM's Open returns the same error class for both. We surface a single opaque error (ErrDecryptOpaque) so a malicious caller cannot distinguish the two via the response. Header-level errors (bad magic, unknown version, KDF/cipher id) report specifically because they cannot leak passphrase information.
Index ¶
Constants ¶
const ( // Magic is the 8-byte ASCII envelope magic ("CVRTBK01"). Magic = "CVRTBK01" // MagicLen is len(Magic) — the prefix length the restore/list readers peek. MagicLen = 8 // HdrLen is the fixed serialized header length (magic+version+kdf+iters+salt+cipher+nonce). HdrLen = 8 + 1 + 1 + 4 + 16 + 1 + 12 // = 43 // KDFIters is the PBKDF2-SHA256 iteration count for v1 envelopes. KDFIters = 600_000 // PassphraseEnv names the env var the CLI reads the backup passphrase from. PassphraseEnv = "CULVERT_BACKUP_PASSPHRASE" // #nosec G101 -- env-var NAME, not a credential (NOSONAR) // PassphraseMinLen is the soft floor below which the CLI warns (does not enforce). PassphraseMinLen = 12 )
Exported envelope/policy constants consumed by package main (the backup, restore, list-backups, and CLI flag paths).
Variables ¶
var ErrDecryptOpaque = errors.New("backup decrypt failed (invalid passphrase or tampered backup)")
ErrDecryptOpaque is the single error surfaced for wrong-passphrase AND tampered ciphertext. AES-GCM's Open cannot distinguish the two; we present them identically so an attacker has no oracle.
Functions ¶
func DecryptBlob ¶
DecryptBlob parses the header, derives the key, and opens the ciphertext using the header as AAD. Returns plaintext or ErrDecryptOpaque on any wrong-key or authentication failure. Header-level validation errors (bad magic, version, ids, length, iter floor) are returned as their own non-opaque errors because they cannot leak passphrase information.
func EncryptBlob ¶
EncryptBlob seals plaintext (a compressed D1.3a tar.gz) under the given passphrase. Generates a fresh salt and nonce, derives the AES-256 key via PBKDF2-SHA256, and seals with the header as AAD. Returns the on-disk blob (header || ciphertext+tag).
Memory: peak usage is ~plaintext+ciphertext (≈ 2× compressed-tarball size). Acceptable for one-shot admin operations on the backup sizes CLAUDE.md documents (well under 100 MB). Future v2 may switch to chunked AEAD for streaming.
func IsEncryptedBlob ¶
IsEncryptedBlob returns true iff prefix begins with the D1.4 magic. Used by the restore reader to decide between the encrypted path and the D1.3a unencrypted tar.gz path.
Types ¶
This section is empty.