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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
//! Append-shaped writing into a fixed slot.
//!
//! > **Import path:** `use secure_gate::SlotWriter;`
//!
//! The sized constructors — [`Fixed::new_with`](crate::Fixed::new_with) and
//! [`Dynamic::new_with`](crate::Dynamic::new_with) — hand the closure a slot whose
//! length is already decided, which is what makes growth, and therefore the
//! reallocation hazard, inexpressible. The cost is that concatenation becomes offset
//! arithmetic:
//!
//! ```text
//! slot[..7].copy_from_slice(LABEL);
//! slot[7..7 + id.len()].copy_from_slice(suite_id); // and so on, by hand
//! ```
//!
//! That trades a reallocation hazard for an **offset** hazard, and in wire-format code
//! it is the worse of the two: a wrong bound derives the wrong key and still runs. This
//! type keeps the slot as the primitive and restores the append shape on top of it.
/// A write cursor over a fixed-length slot.
///
/// Writes advance a position and **panic on overrun** rather than truncating or wrapping
/// — running past the end of a secret's buffer is a bug, not a condition to recover
/// from, and the panic names the shortfall.
///
/// The slot is not owned here. Zeroization remains the wrapper's, so a `SlotWriter`
/// borrowed from inside `new_with` inherits the same panic-safety: unwinding drops the
/// wrapper's `Zeroizing`, which wipes whatever was written.
///
/// # Examples
///
/// RFC 9180-style labeled concatenation, where the length is the sum of the parts. Here
/// every part has a length the compiler knows, so the total is a constant and the
/// wrapper is a [`Fixed`](crate::Fixed):
///
/// ```rust
/// use secure_gate::{Fixed, RevealSecret, SlotWriter};
///
/// const LABEL: &[u8] = b"HPKE-v1";
/// let suite_id = [0xAAu8; 3];
/// let ikm = [0xBBu8; 6];
///
/// let secret = Fixed::<[u8; 16]>::new_with(|slot| {
/// let mut w = SlotWriter::new(slot);
/// w.push_slice(LABEL);
/// w.push_slice(&suite_id);
/// w.push_slice(&ikm);
/// assert!(w.is_full());
/// });
///
/// assert_eq!(&secret.expose_secret()[..7], LABEL);
/// ```
///
/// The same concatenation when the parts are sized at runtime. This is the commoner
/// shape for a real wire format — a suite identifier and an IKM that arrived over the
/// network have no length the compiler can see — and the only thing that changes is
/// which constructor takes the sum: [`Dynamic::new_with`](crate::Dynamic::new_with)
/// accepts the total as a value, so `SlotWriter` serves it exactly as it serves the
/// constant case. There is no reason to reach for an oversized `Fixed` plus a separate
/// logical length:
///
/// ```rust
/// # #[cfg(feature = "alloc")] {
/// use secure_gate::{Dynamic, RevealSecret, SlotWriter};
///
/// const LABEL: &[u8] = b"HPKE-v1";
/// let suite_id: Vec<u8> = vec![0xAA; 3]; // lengths known only at run time
/// let ikm: Vec<u8> = vec![0xBB; 6];
///
/// let total = LABEL.len() + suite_id.len() + ikm.len();
///
/// let secret = Dynamic::<Vec<u8>>::new_with(total, |slot| {
/// let mut w = SlotWriter::new(slot);
/// w.push_slice(LABEL);
/// w.push_slice(&suite_id);
/// w.push_slice(&ikm);
/// assert!(w.is_full()); // the sum and the writes agreed
/// });
///
/// assert_eq!(secret.expose_secret().len(), total);
/// assert_eq!(&secret.expose_secret()[..7], LABEL);
/// # }
/// ```
///
/// # The zero tail is inherited, not imposed
///
/// A `SlotWriter` never clears anything. It copies the bytes it is handed and leaves
/// every other byte exactly as it found it, so what an unwritten tail holds is a fact
/// about the slot you passed to [`new`](Self::new), not a guarantee this type supplies.
///
/// Over a slot from a sized constructor that fact is zeros, by documented contract
/// rather than by luck: [`Fixed::new_with`](crate::Fixed::new_with) starts from
/// [`SentinelValue::sentinel_value`](crate::SentinelValue::sentinel_value), which is
/// `[0u8; N]` for a byte array, and [`Dynamic::new_with`](crate::Dynamic::new_with)
/// allocates `len` zero bytes and documents the zero fill as input callers may rely on.
/// For those two slots — and for a stack buffer you zeroed yourself — a short write is
/// not undefined; it is a zero tail, which is frequently what a format wants:
///
/// ```rust
/// use secure_gate::{Fixed, RevealSecret, SlotWriter};
///
/// let key = Fixed::<[u8; 16]>::new_with(|slot| {
/// let mut w = SlotWriter::new(slot);
/// w.push_slice(&[1, 2, 3, 4, 5]); // 40-bit key, zero-padded to 16
/// });
/// assert_eq!(&key.expose_secret()[5..], &[0u8; 11]);
/// ```
///
/// Over any other slot the tail is whatever was there before, and two of those are
/// traps. One is a scratch buffer reused for a second key. The other is rewriting a
/// wrapper in place — `SlotWriter::new(&mut secret.expose_secret_mut()[..])` — where the
/// bytes already in the slot are the *previous secret*. Write fewer bytes than the
/// previous pass did and the untouched tail is stale key material that the next
/// derivation consumes as input: a wrong key that still runs, which is the failure mode
/// this type exists to remove rather than to introduce by a different route. Build a new
/// wrapper with a sized `new_with` instead of rewriting one in place; failing that, zero
/// the slot before wrapping it, or write it to [`is_full`](Self::is_full) so that no byte
/// of the old contents survives.
///
/// # `Debug` shows the cursor, never the slot
///
/// `{:?}` prints `position`, `remaining` and `len`, and no byte of the slot — see the
/// `Debug` impl below for the exact rendering and for why it is hand-written.
/// Prints cursor state — `position`, `remaining`, `len` — and never a byte of the slot.
///
/// Hand-written rather than derived, and the difference is not cosmetic. The one data
/// field here is `slot: &'a mut [u8]`, so a derived `Debug` printed the borrowed bytes
/// verbatim: a writer six bytes into an eight-byte slot rendered as
/// `SlotWriter { slot: [222, 173, 190, 239, 202, 254, 0, 0], pos: 6 }` — live secret
/// material, in decimal, in whatever log line the `{:?}` went to. A `SlotWriter` is alive
/// exactly while the slot holds that material, and the case this type is built for, a
/// wire format whose offsets are hard to get right, is precisely the case where someone
/// reaches for `dbg!`. Every other secret-bearing type in the crate hand-writes `Debug`
/// for the same reason; [`Fixed`](crate::Fixed), [`Dynamic`](crate::Dynamic) and
/// [`EncodedSecret`](crate::EncodedSecret) print a flat `[REDACTED]`.
///
/// This one prints the cursor rather than `[REDACTED]` because the cursor is the thing
/// worth seeing when an offset is wrong, and it is not secret material: `len` is the slot
/// length the caller chose and passed to the constructor, and `position` counts the bytes
/// that caller wrote. None of the three numbers depends on the *value* of any byte,
/// written or unwritten — which is what makes the output safe to put in a log and safe
/// to rely on.
///
/// ```rust
/// use secure_gate::{Fixed, SlotWriter};
///
/// let secret = Fixed::<[u8; 8]>::new_with(|slot| {
/// let mut w = SlotWriter::new(slot);
/// w.push_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]);
///
/// let shown = format!("{:?}", w);
/// assert_eq!(shown, "SlotWriter { position: 6, remaining: 2, len: 8 }");
/// assert!(!shown.contains("222"), "0xDE is 222, which the derived impl printed");
/// });
/// assert_eq!(format!("{:?}", secret), "[REDACTED]");
/// ```
/// Writes into the slot, failing rather than panicking when it is full.
///
/// Present so a slot can be handed to code that writes through [`std::io::Write`] — a
/// serializer, an encoder, a hasher's output sink. Overrun is reported as
/// [`ErrorKind::WriteZero`](std::io::ErrorKind::WriteZero) rather than as a panic,
/// because a `Write` implementation returning an error is how that interface expects to
/// refuse.
///
/// Note the difference from [`push_slice`](SlotWriter::push_slice), which panics: this
/// path exists for foreign code that cannot be asked to respect a length, so an error is
/// the only way to tell it to stop.
///
/// # Which call reports the overrun, and what the slot holds when it does
///
/// Panic-versus-error is not the whole of the difference. The other half is *when* the
/// refusal arrives and what is in the slot by then. [`push_slice`](SlotWriter::push_slice)
/// checks the whole length before it copies anything, so an overrun there leaves the slot
/// byte-for-byte as it was. `write` cannot do that and stay inside its contract:
/// [`Write::write`](std::io::Write::write) is defined to accept a prefix and report how
/// much it took, so an oversized `buf` is a **short write** — `min(buf.len(),
/// remaining())` bytes are copied, `Ok(n)` comes back, and `WriteZero` appears only on a
/// *later* call, once the slot has no room left at all.
///
/// | Call | Oversized payload | Slot afterwards |
/// |------|-------------------|-----------------|
/// | [`push_slice`](SlotWriter::push_slice) | panics, naming the shortfall | untouched — the check runs before any copy |
/// | [`write_all`](std::io::Write::write_all) | `Err` of kind `WriteZero` | holds the bytes that fit |
/// | [`write`](std::io::Write::write) | `Ok(n)` with `n < buf.len()`, no error | holds the bytes that fit |
///
/// So `write_all` is the call that surfaces overrun as an error, and it is the one to
/// prefer. A bare `write` whose return value is ignored surfaces nothing at all: the
/// weaker signal in the likelier case, since dropping a `write` count on the floor is the
/// mistake `write_all` exists to prevent.
///
/// Either way, and unlike `push_slice`, a fill driven through this impl can leave a
/// **partial value** in the slot: the bytes that fit, with whatever the slot already held
/// behind them. That is indistinguishable, by inspection of the slot, from a deliberate
/// short write. Assert the length you meant to reach instead —
/// [`is_full`](SlotWriter::is_full) when the writes were supposed to fill the slot,
/// [`position`](SlotWriter::position) or [`remaining`](SlotWriter::remaining) when they
/// were not — which is the same post-condition the `push_slice` example asserts, applied
/// to a fill you did not write yourself:
///
/// ```rust
/// use std::io::{ErrorKind, Write};
/// use secure_gate::{Fixed, RevealSecret, SlotWriter};
///
/// let truncated = Fixed::<[u8; 4]>::new_with(|slot| {
/// let mut w = SlotWriter::new(slot);
///
/// let err = w.write_all(&[1, 2, 3, 4, 5, 6]).unwrap_err();
/// assert_eq!(err.kind(), ErrorKind::WriteZero);
///
/// // The slot is NOT untouched: four bytes were taken before the refusal.
/// assert!(w.is_full());
/// assert_eq!(w.remaining(), 0);
/// });
/// assert_eq!(truncated.expose_secret(), &[1, 2, 3, 4]);
/// ```