Expand description
suminuri-wire — 墨塗り, “ink-blacked”: the sops-compatible encrypted-file
wire format as a typed border.
A 墨塗り document is one whose structure stays readable while each value is blacked out in place. That is exactly what a sops file is, and it is the whole reason the format exists rather than encrypting the file as a blob: the keys, the shape and the diff survive.
§What this crate is
The pure half. It owns the bytes-on-disk contract and nothing else — no
filesystem, no clock, no randomness beyond IV generation, no key providers,
no CLI. Everything that touches the outside world lives behind the
Environment seam in suminuri proper.
Every claim encoded here was measured against the sops v3 Go source and
then proven end-to-end against the operator’s own live files: 272 leaves of
nix/secrets.yaml decrypt with a byte-exact MAC, and the one file whose data
key is not ours is refused rather than silently passed. The full spec, with
the file:line citations behind each rule, is docs/WIRE-FORMAT.md.
§The illegal states that have no code path here
Named first, then removed — the ordering UNREPRESENTABILITY.md §IV asks for.
- A 12-byte nonce. sops uses a 32-byte GCM nonce (
Iv::LEN). Every mainstream AES-GCM API defaults to 12, so the wrong choice compiles, runs, and produces a file nothing can open. HereIv::generateis the only way to make an IV for encryption and it is[u8; 32]by type — there is no constructor that takes a length. truly-unrep. - An AAD built by hand. The additional authenticated data is the leaf’s
dotted path joined by
:with a trailing:, and sequence indices are excluded.Aadhas noFrom<String>; the only way to get one isAadPath::aad, which always appends the colon, andAadPathhas no method that takes an index. truly-unrep (absent method). - Using a tree whose MAC was never checked. Decryption yields
Unverified<T>, whose only safe exit isUnverified::verify. The--ignore-macescape exists but is spelledUnverified::into_inner_ignoring_mac— one greppable token, never a default. truly-unrep for the accidental case. - A MAC compared in non-constant time. Upstream compares with Go’s
!=.Mac’s inner string is private and itsPartialEqroutes throughsubtle::ConstantTimeEq, so there is no other comparison to reach for. Same verdict, no timing signal. truly-unrep. - Declared recipients that disagree with the wrapped keys. This is not a
hypothetical:
nix/.sops.yamlcarried a declared admin-recovery recipient forusers/gabi/secrets.yamlfor two weeks that was never in the ciphertext, because only a current recipient can re-wrap a data key.Metadata’s key arrays are derived from theWrappedKeyset viaMetadata::from_wrapped— they are not independently settable, so a declaration that outruns the ciphertext cannot be emitted. truly-unrep. - A plaintext in a
String. Leaf plaintext isPlaintext, which holdsZeroizing<Vec<u8>>, has noDisplay, noDeref<str>, and printsPlaintext(*** N bytes)underDebug. Reading it takes the greppablePlaintext::expose. parse-time-rejected — an author can still callexpose(); the ceiling is that Rust cannot forbid a named call (C1).
§What is deliberately reproduced rather than fixed
Two upstream quirks are bugs we must speak anyway, because the wire is the wire (the magma posture: speak the wire, own the executor).
- AAD path collision. Keys are joined unescaped, so
{"a:b": {"c": v}}and{"a": {"b:c": v}}produce the same AAD. Reproduced; flagged byAadPath::has_ambiguous_componentso a caller can refuse. - IV reuse by design.
IvStashre-uses the IV recorded for a(plaintext, aad)pair so an unchanged value re-encrypts to identical bytes — which is what keepseditdiffs small. Without it every edit rewrites every line.
Structs§
- Aad
- The finished AAD string for one leaf. Opaque on purpose.
- AadPath
- The stack of string mapping keys from the document root down to a leaf.
- AgeKey
- An
ageentry in thesops.agearray. - DataKey
- The 32-byte symmetric key every leaf in one file is encrypted under.
- Encrypted
Leaf - A parsed
ENC[…]leaf: the three base64 fields plus the type tag. - Encryption
Selector - The compiled form of a file’s encryption policy.
- Iv
- A nonce for writing. Always 32 bytes, by type.
- IvStash
- Remembers the IV used for each
(plaintext, aad)pair so re-encrypting an unchanged value reproduces its exact previous ciphertext. - Mac
- A computed file MAC: 128 uppercase hex characters.
- MacAccumulator
- Accumulates leaf plaintexts into a file MAC, in walk order.
- Metadata
- The
sops:metadata block. - Plaintext
- A leaf’s plaintext bytes, together with the type they render as.
- Unverified
- A decrypted value whose file MAC has not been checked yet.
- Wrapped
Key - One recipient’s wrapped copy of the data key.
Enums§
- KeyProvider
- A key provider that can wrap the data key.
- Leaf
Type - The datatype tag carried in
type:. - Selection
- The verdict for one leaf.
- Wire
Error - Everything that can go wrong inside the wire border.
Constants§
- DEFAULT_
UNENCRYPTED_ SUFFIX - sops’s default when no selector at all is configured.
- FORMAT_
VERSION - The sops format version this crate writes into
sops.version. - MAC_
ONLY_ ENCRYPTED_ SEED sha256(b"sops"), the pre-seed for amac_only_encrypteddigest.
Functions§
- decrypt_
leaf - Decrypt one leaf.
- encrypt_
leaf - Encrypt one leaf.
- mac_
field_ aad - The AAD under which the
sops.macfield itself is encrypted: the RFC 3339 rendering ofsops.lastmodified, verbatim from the file. - seal_
mac_ field - Encrypt a computed MAC into the
sops.macfield value. - verify_
mac_ field - Decrypt a file’s
macfield and compare it against a recomputed MAC.