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
//! Owned zeroizing wrapper for encoded secret strings.
//!
//! > **Import path:** `use secure_gate::EncodedSecret;`
//!
//! [`EncodedSecret`] wraps `Zeroizing<String>` with `Debug` → `[REDACTED]`. It is
//! returned by every encoding method (`to_hex`, `to_base32`, `to_base64url`,
//! `try_to_bech32`, `try_to_bech32m`, and the `_sized::<N>` forms of the last two —
//! only bech32 and bech32m take a code length).
//!
//! There is no unprotected encoder variant. An encoded secret is a second full copy of
//! the secret in a longer, human-readable alphabet, so it is wiped by default; public
//! encodings (addresses, transaction IDs) come back in the same wrapper. Name
//! [`into_inner`](EncodedSecret::into_inner) when you genuinely want a plain `String`.
//!
//! This is **not** a secret wrapper like [`Fixed`](crate::Fixed) / [`Dynamic`](crate::Dynamic)
//! — it is a zeroizing `String` wrapper for encoded output. Its only accessor is
//! `Deref<Target = str>`, plus the two named consumers [`into_inner`](EncodedSecret::into_inner)
//! and [`into_zeroizing`](EncodedSecret::into_zeroizing).
//!
//! `Deref` here is a deliberate exception to the crate's no-`Deref` rule, and the
//! alternative (drop it, add `as_str()`, and close `.to_string()`) was considered and
//! rejected. Design record:
//! [`docs/encoded_secret_deref.md`](https://github.com/Slurp9187/secure-gate/blob/main/docs/design/encoded_secret_deref.md)
//! — a repository file, not part of the published crate, so this is a link and not a path.
//!
//! # No `Display`
//!
//! `EncodedSecret` deliberately does **not** implement `Display`, so `{}` in a format
//! string is a compile error. `Debug` printing `[REDACTED]` teaches callers that the
//! type is safe to put in a log line; a transparent `Display` on the same type would
//! then punish exactly the callers who checked. `tracing::info!("token: {tok}")` and
//! `format!("{tok}")` are the accidents worth preventing, and they are the ones a
//! missing `Display` prevents.
//!
//! Writing the encoded value out is still one deref away, and deliberately explicit:
//!
//! ```rust
//! # #[cfg(all(feature = "encoding-hex", feature = "alloc"))] {
//! use secure_gate::{Fixed, ToHex};
//!
//! let encoded = Fixed::new([0xABu8; 4]).to_hex();
//!
//! // Intentional: name the deref.
//! let line = format!("{}", &*encoded);
//! assert_eq!(line, "abababab");
//!
//! // Accidental: does not compile.
//! // let line = format!("{}", encoded);
//! # }
//! ```
//!
//! This does **not** stop copying by convenience. `Deref<Target = str>` still gives you
//! `str::to_string()` and `.to_owned()`, both of which produce an ordinary unzeroized
//! `String`. They are not, however, the hand-off, and the difference is worth knowing:
//!
//! | | `.to_string()` / `.to_owned()` | [`into_inner()`](EncodedSecret::into_inner) |
//! |-----------------------|--------------------------------|---------------------------------------------|
//! | What happens | Copies into a new `String` | Moves *this* `String` out (`mem::take`) |
//! | The wrapper afterwards| Still held, still wiped on drop| Consumed |
//! | Copies of the secret | Two, until the wrapper drops | One, and it is yours |
//! | In an audit sweep | Noisy — a `str` method | A named exit |
//!
//! So reach for `into_inner()` when an API wants an owned `String`: it is the same
//! ownership transfer as [`RevealSecret::into_inner`](crate::RevealSecret::into_inner),
//! and it leaves one copy rather than two. Treat `.to_string()` as *I copied it on
//! purpose*, and sweep for it during an encoding audit. What the type is actually for
//! is `&*encoded` into the drivers that bind `&str`. See
//! [Where accident-prevention ends](crate#where-accident-prevention-ends).
//!
//! # Every `str` method reached through `Deref` exits protection, not just `.to_string()`
//!
//! The table above singles out `.to_string()` / `.to_owned()` because those are the
//! obvious copies. They are not the only ones. `Deref<Target = str>` exposes *every*
//! method `str` has, and a good many of them — `to_ascii_uppercase`,
//! `to_ascii_lowercase`, `to_lowercase`, `to_uppercase`, `replace`, `repeat`, `trim()`
//! followed by an owning call, and more — return a freshly allocated, ordinary `String`.
//! Each one ends protection the instant it runs, and nothing at the call site marks the
//! exit:
//!
//! ```rust
//! # #[cfg(all(feature = "encoding-bech32", feature = "alloc"))] {
//! use secure_gate::{Fixed, ToBech32m, Case};
//!
//! let key = Fixed::new([0xABu8; 32]);
//! let encoded = key.try_to_bech32m("key", Case::Lower).unwrap(); // EncodedSecret -- protected
//! let shouted = encoded.to_ascii_uppercase(); // plain String -- escaped, unwiped
//! # let _ = shouted;
//! # }
//! ```
//!
//! `shouted` is a second full copy of the key material: no zeroize-on-drop, no redacted
//! `Debug`, and the receiver reads exactly like the `EncodedSecret` it was called on one
//! line up. Nothing in the syntax distinguishes this call from one that stayed inside
//! protection.
//!
//! This is not hypothetical. A consumer of this crate hit it twice in one codebase, on
//! the same idiom, with opposite outcomes. One call site piped `try_to_bech32m(..)`
//! straight into `.to_ascii_uppercase()` and shipped it, leaking a database key as the
//! uppercased copy neither wrapper ever touched — the `EncodedSecret` it was called on
//! wiped itself on drop, which is what makes the surviving copy so easy to miss. A
//! second call site, reaching for the same encoder, re-wrapped the result before using
//! it: `Zeroizing::new(encoded.to_ascii_uppercase())`. Same crate, same method chain,
//! two different outcomes in one tree — which is what makes this a trap and not
//! carelessness. Both call sites compile. Both type-check. Only one of them is safe, and
//! nothing in the types says which.
//!
//! Two ways to stay inside protection:
//!
//! - **Do the transform inside the encoder, where one exists.**
//! [`to_hex_upper`](crate::ToHex::to_hex_upper) exists precisely so callers never need
//! `.to_hex().to_ascii_uppercase()`, and the bech32 / bech32m encoders take a
//! [`Case`](crate::Case) argument for the same reason — ask the encoder for the case
//! you want instead of asking `str` to convert it afterward.
//! - **Re-wrap the result yourself** when no such parameter exists:
//! `Zeroizing::new(encoded.some_str_method())`. That is one allocation you chose and
//! can account for, in place of one the type system quietly declined to track.
//!
//! This is the same shape of hole as the one already closed for this exact type in
//! [`EncodableBytes`](crate::EncodableBytes#why-the-extra-bound-exists): there, an
//! `AsRef<[u8]>`-only blanket let `encoded.to_hex()` compile on an `EncodedSecret` and
//! hex-encode the encoded *text* — a 32-byte key came back as 124 hex characters, with
//! nothing to flag it as wrong. That hole was closed with a trait bound, because the
//! encoders are this crate's own API and a bound can gate them. This one cannot close
//! the same way: `str`'s inherent methods are not a trait this crate can add a bound to,
//! so the deref stays reachable by construction — see
//! [No `Display`](crate::EncodedSecret#no-display) above for why the deref itself stays.
//! The boundary here is not a compile error; it is knowing it precisely — any `str`
//! method reached through this type's `Deref` produces unprotected output, full stop —
//! and giving every one of them the scrutiny the table above already asks for
//! `.to_string()`.
/// Owned wrapper for encoded secret strings: zeroizes on drop, redacts `Debug`, and
/// has no `Display`.
///
/// Every encoding method returns this type — `to_hex`, `to_hex_upper`, `to_base32`,
/// `to_base64url`, `try_to_bech32`, `try_to_bech32m`, and the `_sized::<N>` forms of
/// the last two (only bech32 and bech32m take a code length) — on
/// [`Fixed`](crate::Fixed), [`Dynamic`](crate::Dynamic), and any byte-shaped input.
/// There is no unprotected variant to choose between: an encoded secret is a second
/// full copy of the secret, so it is wiped by default, and
/// [`into_inner`](EncodedSecret::into_inner) is the named call that ends that.
;
// No `AsRef<str>` / `AsRef<[u8]>`: `Deref<Target = str>` yields `&str` by coercion, by
// `&*encoded`, and through method resolution, so for those uses the impls were doors
// onto a room `Deref` already opens.
//
// They were not equivalent, though, and the difference is worth stating because it is
// the one thing removing them broke. Deref coercion applies at a coercion site; it does
// not satisfy a generic bound. So `fs::write(path, &encoded)` — or any
// `impl AsRef<[u8]>` parameter — no longer compiles, and the caller writes
// `encoded.as_bytes()` instead. That is a deliberate cost: a generic byte sink is
// exactly the call that should name the extraction rather than have it inferred.
//
// Removing them did not, on its own, close the accidental re-encode: method resolution
// derefs to `str`, and `str: AsRef<[u8]>` satisfied the old encoder blanket impls, so
// `encoded.to_hex()` compiled and hex-encoded the encoded text. That reachability comes
// from `Deref`, which is the type's primary accessor and stays. The bound closed it
// instead — the encoders now require `AsRef<[u8]> + EncodableBytes`, and `str` does not
// implement `EncodableBytes`, so the second encode is a compile error. Pinned by the
// `encoded_secret_no_reencode` compile-fail test. The surviving boundary is documented
// at [Where accident-prevention ends](crate#where-accident-prevention-ends).