shardline_protocol/
security.rs1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4use subtle::ConstantTimeEq;
5use zeroize::{Zeroize, ZeroizeOnDrop};
6
7#[derive(Clone, Eq, Deserialize, Zeroize, ZeroizeOnDrop)]
21pub struct SecretBytes(Vec<u8>);
22
23impl PartialEq for SecretBytes {
24 fn eq(&self, other: &Self) -> bool {
25 self.0.ct_eq(&other.0).into()
26 }
27}
28
29impl SecretBytes {
30 #[must_use]
32 pub const fn new(secret: Vec<u8>) -> Self {
33 Self(secret)
34 }
35
36 #[must_use]
38 pub fn from_slice(secret: &[u8]) -> Self {
39 Self(secret.to_vec())
40 }
41
42 #[must_use]
44 pub fn expose_secret(&self) -> &[u8] {
45 &self.0
46 }
47
48 #[must_use]
50 pub const fn len(&self) -> usize {
51 self.0.len()
52 }
53
54 #[must_use]
56 pub const fn is_empty(&self) -> bool {
57 self.0.is_empty()
58 }
59}
60
61impl AsRef<[u8]> for SecretBytes {
62 fn as_ref(&self) -> &[u8] {
63 self.expose_secret()
64 }
65}
66
67impl From<Vec<u8>> for SecretBytes {
68 fn from(secret: Vec<u8>) -> Self {
69 Self(secret)
70 }
71}
72
73impl fmt::Debug for SecretBytes {
74 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75 formatter.write_str("***")
76 }
77}
78
79#[derive(Clone, Eq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
92pub struct SecretString(String);
93
94impl PartialEq for SecretString {
95 fn eq(&self, other: &Self) -> bool {
96 self.0.as_bytes().ct_eq(other.0.as_bytes()).into()
97 }
98}
99
100impl SecretString {
101 #[must_use]
103 pub const fn new(secret: String) -> Self {
104 Self(secret)
105 }
106
107 #[must_use]
112 pub fn from_secret(secret: &str) -> Self {
113 Self(secret.to_owned())
114 }
115
116 #[must_use]
118 pub fn expose_secret(&self) -> &str {
119 &self.0
120 }
121
122 #[must_use]
124 pub const fn is_empty(&self) -> bool {
125 self.0.is_empty()
126 }
127}
128
129impl AsRef<str> for SecretString {
130 fn as_ref(&self) -> &str {
131 self.expose_secret()
132 }
133}
134
135impl fmt::Debug for SecretString {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter.write_str("***")
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::{SecretBytes, SecretString};
144
145 #[test]
146 fn secret_bytes_debug_redacts_contents() {
147 let secret = SecretBytes::from_slice(b"test-signing-key-32-bytes-long!!");
148
149 assert_eq!(format!("{secret:?}"), "***");
150 }
151
152 #[test]
153 fn secret_bytes_exposes_underlying_bytes() {
154 let secret = SecretBytes::from_slice(b"test-signing-key-32-bytes-long!!");
155
156 assert_eq!(secret.expose_secret(), b"test-signing-key-32-bytes-long!!");
157 }
158
159 #[test]
160 fn secret_string_debug_redacts_contents() {
161 let secret = SecretString::from_secret("bootstrap-token");
162
163 assert_eq!(format!("{secret:?}"), "***");
164 }
165
166 #[test]
167 fn secret_string_exposes_underlying_text() {
168 let secret = SecretString::from_secret("bootstrap-token");
169
170 assert_eq!(secret.expose_secret(), "bootstrap-token");
171 }
172
173 #[test]
174 fn secret_bytes_new_wraps_owned() {
175 let data = vec![1, 2, 3, 4];
176 let secret = SecretBytes::new(data);
177 assert_eq!(secret.expose_secret(), &[1, 2, 3, 4]);
178 }
179
180 #[test]
181 fn secret_bytes_len_and_is_empty() {
182 let empty = SecretBytes::new(Vec::new());
183 assert!(empty.is_empty());
184 assert_eq!(empty.len(), 0);
185
186 let non_empty = SecretBytes::from_slice(b"abc");
187 assert!(!non_empty.is_empty());
188 assert_eq!(non_empty.len(), 3);
189 }
190
191 #[test]
192 fn secret_bytes_as_ref() {
193 use std::convert::AsRef;
194 let secret = SecretBytes::from_slice(b"hello");
195 let bytes: &[u8] = secret.as_ref();
196 assert_eq!(bytes, b"hello");
197 }
198
199 #[test]
200 fn secret_string_new_wraps_owned() {
201 let secret = SecretString::new("owned".to_owned());
202 assert_eq!(secret.expose_secret(), "owned");
203 }
204
205 #[test]
206 fn secret_string_is_empty() {
207 let empty = SecretString::new(String::new());
208 assert!(empty.is_empty());
209
210 let non_empty = SecretString::from_secret("data");
211 assert!(!non_empty.is_empty());
212 }
213
214 #[test]
215 fn secret_string_as_ref() {
216 use std::convert::AsRef;
217 let secret = SecretString::from_secret("text");
218 let s: &str = secret.as_ref();
219 assert_eq!(s, "text");
220 }
221
222 #[test]
223 fn secret_bytes_clone_produces_equal_data() {
224 let a = SecretBytes::from_slice(b"secret-data");
225 let b = a.clone();
226 assert_eq!(a.expose_secret(), b.expose_secret());
227 }
228
229 #[test]
230 fn secret_string_clone_produces_equal_data() {
231 let a = SecretString::from_secret("secret-text");
232 let b = a.clone();
233 assert_eq!(a.expose_secret(), b.expose_secret());
234 }
235
236 #[test]
237 fn secret_bytes_partial_eq_compares_content() {
238 let a = SecretBytes::from_slice(b"same");
239 let b = SecretBytes::from_slice(b"same");
240 let c = SecretBytes::from_slice(b"different");
241 assert_eq!(a, b);
242 assert_ne!(a, c);
243 }
244
245 #[test]
246 fn secret_string_partial_eq_compares_content() {
247 let a = SecretString::from_secret("same");
248 let b = SecretString::from_secret("same");
249 let c = SecretString::from_secret("different");
250 assert_eq!(a, b);
251 assert_ne!(a, c);
252 }
253
254 #[test]
255 fn secret_bytes_len_various_sizes() {
256 assert_eq!(SecretBytes::new(vec![]).len(), 0);
257 assert_eq!(SecretBytes::from_slice(b"a").len(), 1);
258 assert_eq!(SecretBytes::from_slice(b"hello").len(), 5);
259 assert_eq!(SecretBytes::from_slice(&[0; 100]).len(), 100);
260 }
261
262 #[test]
263 fn secret_string_empty_vs_non_empty() {
264 assert!(SecretString::new(String::new()).is_empty());
265 assert!(!SecretString::from_secret("data").is_empty());
266 }
267
268 #[test]
269 fn secret_bytes_is_empty_various() {
270 assert!(SecretBytes::new(vec![]).is_empty());
271 assert!(!SecretBytes::from_slice(b"x").is_empty());
272 }
273
274 #[test]
275 fn secret_bytes_debug_no_content_leak() {
276 let secret = SecretBytes::from_slice(b"hunter2-password-12345");
277 let debug = format!("{secret:?}");
278 assert_eq!(debug, "***");
279 assert!(!debug.contains("hunter2"));
280 }
281
282 #[test]
283 fn secret_string_debug_no_content_leak() {
284 let secret = SecretString::from_secret("my-secret-api-token");
285 let debug = format!("{secret:?}");
286 assert_eq!(debug, "***");
287 assert!(!debug.contains("api-token"));
288 }
289
290 #[test]
291 fn secret_bytes_zeroize_clears_memory() {
292 use zeroize::Zeroize;
293 let mut secret = SecretBytes::from_slice(b"hello-world");
294 assert!(!secret.is_empty());
295 assert_eq!(secret.len(), 11);
296 secret.zeroize();
298 assert!(secret.is_empty());
299 assert_eq!(secret.len(), 0);
300 assert!(secret.expose_secret().is_empty());
301 }
302}