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
//! [`Sensitive<T>`]: a secret-value wrapper that redacts `Debug`/`Display`,
//! never serializes, and zeroizes its contents on drop (#1169).
//!
//! Config structs across the workspace hold credential/key fields — an LLM
//! provider's bearer key, a wallet signer key, an HMAC challenge secret — as
//! plain `String`s today. A plain `String` field prints its raw value from a
//! derived `Debug` impl, from any accidental `{}`/`{:?}` in a log line, and
//! from a derived `Serialize` impl the moment the enclosing struct is ever
//! serialized (a debug endpoint, a forensics dump, a stray `serde_json::to_string`).
//! [`Sensitive<T>`] closes all three holes at the type level: it only ever
//! prints `Sensitive(<redacted>)`, it deliberately has no `Serialize` impl (so
//! a struct that embeds one fails to compile if something tries to derive
//! `Serialize` over it, rather than silently leaking), and its value is
//! wiped from memory as soon as it drops.
//!
//! Reads the secret back out only through the explicit
//! [`expose`](Sensitive::expose) / [`expose_secret`](Sensitive::expose_secret)
//! accessors (identical; `expose_secret` matches the naming the `secrecy`
//! crate uses, so call sites read the same regardless of which wrapper backs
//! them) — every use site is grep-able and visibly intentional.
//!
//! ## Why a local newtype instead of `secrecy::SecretString`
//!
//! The `secrecy` crate (already resolved transitively in this workspace, via
//! `kube-client`) redacts `Debug` and zeroizes on drop, but deliberately does
//! **not** implement `Display` — printing a secret via `{}` is exactly the
//! footgun it exists to prevent, so it forces every read through
//! `expose_secret()`. Issue #1169 asks for a redacted `Display` too (so a
//! stray `format!("{secret}")` — not just `{:?}` — still can't leak), and for
//! the wrapper to print as `Sensitive(<redacted>)` specifically. Bridging
//! that gap by wrapping `SecretString` in another newtype would add a layer
//! of indirection with no upside over implementing the same
//! zeroize-on-drop + redacted-formatting contract directly against the
//! `zeroize` crate, which this workspace already depends on
//! (`crates/passkey`). A thin local type also stays generic over any `T:
//! Zeroize` (not just `String`), so it can wrap a future non-`String` secret
//! (e.g. raw key bytes) without another wrapper.
use fmt;
use Zeroize;
/// A secret value, redacted in `Debug`/`Display` and zeroized on drop.
///
/// Never derive or implement `Serialize` on a type embedding this — that is
/// the point: [`Sensitive`] deliberately has no `Serialize` impl, so a
/// container that tries to derive one over a field of this type fails to
/// compile instead of silently emitting the raw secret.
///
/// Deserializing (reading a secret in from TOML/env/CLI) is fine and
/// supported via `serde`'s `Deserialize` — only the write-out direction is
/// closed.
;