// Package crypto implements logarchiver's object encryption. // // # Why not plain OpenPGP-encrypt the whole object? // // The private key lives only in Ben's Vault GPG secrets engine // (vault-plugin-secrets-gpg). That engine's decrypt endpoint does WHOLE-payload // inline decryption only: you POST the entire OpenPGP message (base64 in a JSON // body) and it returns the entire plaintext (base64). There is no session-key / // PKESK extraction and no streaming, so a multi-hundred-MiB archive could not be // retrieved without blowing Vault's request-size limit and buffering everything // twice in the server. // // # The wrapped-DEK envelope (container "LARC1") // // logarchiver therefore does hybrid encryption itself: // // - a fresh random 256-bit Data Encryption Key (DEK) per object; // - the bulk (zstd-compressed NDJSON) is encrypted locally with AES-256-GCM in // independent frames, so decryption streams frame-by-frame; // - only the 32-byte DEK is OpenPGP-encrypted to the engine's PUBLIC key, // producing a small (~hundreds of bytes) standard OpenPGP message. // // On retrieval the CLI sends ONLY that small wrapped-DEK blob to the engine's // decrypt endpoint, recovers the DEK, and streams the bulk locally. The Vault // round-trip is tiny and constant regardless of object size, and the private key // never leaves Vault. The trade-off vs. a single standard OpenPGP object: these // objects are a logarchiver-specific container, not decryptable by a bare `gpg` // even with the private key. The retrieval runbook documents the format. package crypto import ( "crypto/sha256" "fmt" "strings" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" ) // PublicKey is a parsed OpenPGP public key plus its fingerprint (uppercase hex, // no spaces — matching the Vault GPG engine's `%X` fingerprint format). type PublicKey struct { entity *openpgp.Entity Fingerprint string } // LoadPublicKey parses an ASCII-armored (or binary) OpenPGP public key. func LoadPublicKey(data []byte) (*PublicKey, error) { var keyring openpgp.EntityList var err error if strings.Contains(string(data), "BEGIN PGP") { block, berr := armor.Decode(strings.NewReader(string(data))) if berr != nil { return nil, fmt.Errorf("decode armor: %w", berr) } keyring, err = openpgp.ReadKeyRing(block.Body) } else { keyring, err = openpgp.ReadKeyRing(strings.NewReader(string(data))) } if err != nil { return nil, fmt.Errorf("read public key: %w", err) } if len(keyring) == 0 { return nil, fmt.Errorf("no public key found") } ent := keyring[0] if ent.PrimaryKey == nil { return nil, fmt.Errorf("key has no primary public key") } return &PublicKey{ entity: ent, Fingerprint: fmt.Sprintf("%X", ent.PrimaryKey.Fingerprint), }, nil } // digestArmored is used by tests to sanity check key identity independent of // go-crypto internals. func digestArmored(data []byte) string { sum := sha256.Sum256(data) return fmt.Sprintf("%x", sum[:8]) }