1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! `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.
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use Unverified;
/// Everything that can go wrong inside the wire border.
///
/// Note what is *absent*: there is no `Other(String)` arm and no
/// `#[from] anyhow::Error`. A new failure mode has to be named here, which is
/// what keeps a caller's `match` honest.
/// The sops format version this crate writes into `sops.version`.
///
/// We claim the format version, not our own release version, because the field
/// is read by real sops to decide how to parse. Measured against the binary in
/// the operator's profile.
pub const FORMAT_VERSION: &str = "3.12.1";