Skip to main content

Crate suminuri_wire

Crate suminuri_wire 

Source
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.

  1. 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. Here Iv::generate is 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.
  2. 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. Aad has no From<String>; the only way to get one is AadPath::aad, which always appends the colon, and AadPath has no method that takes an index. truly-unrep (absent method).
  3. Using a tree whose MAC was never checked. Decryption yields Unverified<T>, whose only safe exit is Unverified::verify. The --ignore-mac escape exists but is spelled Unverified::into_inner_ignoring_mac — one greppable token, never a default. truly-unrep for the accidental case.
  4. A MAC compared in non-constant time. Upstream compares with Go’s !=. Mac’s inner string is private and its PartialEq routes through subtle::ConstantTimeEq, so there is no other comparison to reach for. Same verdict, no timing signal. truly-unrep.
  5. Declared recipients that disagree with the wrapped keys. This is not a hypothetical: nix/.sops.yaml carried a declared admin-recovery recipient for users/gabi/secrets.yaml for 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 the WrappedKey set via Metadata::from_wrapped — they are not independently settable, so a declaration that outruns the ciphertext cannot be emitted. truly-unrep.
  6. A plaintext in a String. Leaf plaintext is Plaintext, which holds Zeroizing<Vec<u8>>, has no Display, no Deref<str>, and prints Plaintext(*** N bytes) under Debug. Reading it takes the greppable Plaintext::expose. parse-time-rejected — an author can still call expose(); 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 by AadPath::has_ambiguous_component so a caller can refuse.
  • IV reuse by design. IvStash re-uses the IV recorded for a (plaintext, aad) pair so an unchanged value re-encrypts to identical bytes — which is what keeps edit diffs 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 age entry in the sops.age array.
DataKey
The 32-byte symmetric key every leaf in one file is encrypted under.
EncryptedLeaf
A parsed ENC[…] leaf: the three base64 fields plus the type tag.
EncryptionSelector
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.
WrappedKey
One recipient’s wrapped copy of the data key.

Enums§

KeyProvider
A key provider that can wrap the data key.
LeafType
The datatype tag carried in type:.
Selection
The verdict for one leaf.
WireError
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 a mac_only_encrypted digest.

Functions§

decrypt_leaf
Decrypt one leaf.
encrypt_leaf
Encrypt one leaf.
mac_field_aad
The AAD under which the sops.mac field itself is encrypted: the RFC 3339 rendering of sops.lastmodified, verbatim from the file.
seal_mac_field
Encrypt a computed MAC into the sops.mac field value.
verify_mac_field
Decrypt a file’s mac field and compare it against a recomputed MAC.