secret_vault_value/value.rs
1use std::fmt::{Debug, Display, Formatter};
2use std::future::Future;
3use std::hash::{Hash, Hasher};
4use std::str::Utf8Error;
5use subtle::ConstantTimeEq;
6use zeroize::*;
7
8/// A secret held as bytes and zeroized when it is dropped.
9///
10/// `Debug` and `Display` print `***`, so the value does not end up in logs or
11/// panic messages. `Clone` makes a second buffer, and each copy is zeroized on
12/// its own drop. `==` takes time that depends on the lengths only, not on the
13/// bytes.
14///
15/// The value is read in two ways:
16/// - borrowing it with `as_sensitive_*` and `ref_sensitive_value`, which make
17/// no copy;
18/// - lending a copy to a closure with `exposed_in_*`, which makes sure the
19/// copy is zeroized once the closure is done with it.
20///
21/// ```
22/// use secret_vault_value::SecretValue;
23///
24/// let secret_value = SecretValue::from("my-password");
25/// assert_eq!(format!("{secret_value:?}"), "***");
26/// assert_eq!(secret_value.to_string(), "***");
27/// assert_eq!(secret_value.as_sensitive_str(), "my-password");
28/// ```
29#[derive(Zeroize, ZeroizeOnDrop, Default)]
30pub struct SecretValue(Vec<u8>);
31
32impl SecretValue {
33 /// Takes ownership of `src` without copying it.
34 pub fn new(src: Vec<u8>) -> Self {
35 Self(src)
36 }
37
38 /// Borrows the secret bytes.
39 pub fn ref_sensitive_value(&self) -> &Vec<u8> {
40 &self.0
41 }
42
43 /// Borrows the secret bytes mutably.
44 ///
45 /// Be aware that growing the vector may reallocate it, and the old
46 /// allocation is freed without being zeroized. Reserve the capacity up
47 /// front if the value is built in place.
48 pub fn ref_sensitive_value_mut(&mut self) -> &mut Vec<u8> {
49 &mut self.0
50 }
51
52 /// Borrows the secret as a string slice.
53 ///
54 /// # Errors
55 ///
56 /// Returns [`Utf8Error`] if the bytes are not valid UTF-8.
57 ///
58 /// ```
59 /// use secret_vault_value::SecretValue;
60 ///
61 /// assert!(SecretValue::from("my-password").sensitive_value_to_str().is_ok());
62 /// assert!(SecretValue::new(vec![0xff]).sensitive_value_to_str().is_err());
63 /// ```
64 pub fn sensitive_value_to_str(&self) -> Result<&str, Utf8Error> {
65 std::str::from_utf8(&self.0)
66 }
67
68 /// Zeroizes the bytes and leaves the value empty.
69 ///
70 /// ```
71 /// use secret_vault_value::SecretValue;
72 ///
73 /// let mut secret_value = SecretValue::from("my-password");
74 /// secret_value.secure_clear();
75 /// assert!(secret_value.as_sensitive_bytes().is_empty());
76 /// ```
77 pub fn secure_clear(&mut self) {
78 self.0.zeroize();
79 self.0.clear();
80 }
81
82 /// Borrows the secret as a string slice.
83 ///
84 /// # Panics
85 ///
86 /// Panics if the bytes are not valid UTF-8. Use
87 /// [`sensitive_value_to_str`](Self::sensitive_value_to_str) for binary
88 /// secrets.
89 pub fn as_sensitive_str(&self) -> &str {
90 self.sensitive_value_to_str().unwrap()
91 }
92
93 /// Borrows the secret bytes as a slice.
94 pub fn as_sensitive_bytes(&self) -> &[u8] {
95 self.ref_sensitive_value()
96 }
97
98 /// Lends a `String` copy of the secret to `f` and returns the first item of
99 /// its result.
100 ///
101 /// `f` returns a pair `(T, Z)`: `T` is the result, `Z` is whatever must be
102 /// zeroized once `f` returns, usually the `String` it was given. A copy
103 /// that `f` neither returns as `Z` nor zeroizes itself is not zeroized.
104 /// [`exposed_in_as_zstr`](Self::exposed_in_as_zstr) needs no such pair.
105 ///
106 /// If `f` panics, the copy is freed without being zeroized. Use
107 /// [`exposed_in_as_zstr`](Self::exposed_in_as_zstr), which zeroizes it on
108 /// unwinding too.
109 ///
110 /// # Panics
111 ///
112 /// Panics if the bytes are not valid UTF-8.
113 ///
114 /// ```
115 /// use secret_vault_value::SecretValue;
116 ///
117 /// let secret_value = SecretValue::from("my-password");
118 /// let length = secret_value.exposed_in_as_str(|password| (password.len(), password));
119 /// assert_eq!(length, 11);
120 /// ```
121 pub fn exposed_in_as_str<T, Z: Zeroize, FN>(&self, f: FN) -> T
122 where
123 FN: Fn(String) -> (T, Z),
124 {
125 let decoded_as_string = self.sensitive_value_to_str().unwrap().to_string();
126 let (result, mut zeroizable) = f(decoded_as_string);
127 zeroizable.zeroize();
128 result
129 }
130
131 /// Lends a [`Zeroizing<String>`] copy of the secret to `f` and returns its
132 /// result.
133 ///
134 /// The copy is zeroized when `f` drops it.
135 ///
136 /// # Panics
137 ///
138 /// Panics if the bytes are not valid UTF-8.
139 ///
140 /// ```
141 /// use secret_vault_value::SecretValue;
142 ///
143 /// let secret_value = SecretValue::from("my-password");
144 /// let length = secret_value.exposed_in_as_zstr(|password| password.len());
145 /// assert_eq!(length, 11);
146 /// ```
147 pub fn exposed_in_as_zstr<T, FN>(&self, f: FN) -> T
148 where
149 FN: Fn(Zeroizing<String>) -> T,
150 {
151 let decoded_as_string = Zeroizing::new(self.sensitive_value_to_str().unwrap().to_string());
152 f(decoded_as_string)
153 }
154
155 /// Lends a `Vec<u8>` copy of the secret to `f` and returns the first item
156 /// of its result.
157 ///
158 /// The `(T, Z)` pair works as in
159 /// [`exposed_in_as_str`](Self::exposed_in_as_str): `Z` is zeroized once
160 /// `f` returns.
161 ///
162 /// If `f` panics, the copy is freed without being zeroized. Use
163 /// [`exposed_in_as_zvec`](Self::exposed_in_as_zvec), which zeroizes it on
164 /// unwinding too.
165 ///
166 /// ```
167 /// use secret_vault_value::SecretValue;
168 ///
169 /// let secret_value = SecretValue::new(vec![1, 2, 3]);
170 /// let sum = secret_value.exposed_in_as_vec(|bytes| (bytes.iter().sum::<u8>(), bytes));
171 /// assert_eq!(sum, 6);
172 /// ```
173 pub fn exposed_in_as_vec<T, Z: Zeroize, FN>(&self, f: FN) -> T
174 where
175 FN: Fn(Vec<u8>) -> (T, Z),
176 {
177 let (result, mut zeroizable) = f(self.0.clone());
178 zeroizable.zeroize();
179 result
180 }
181
182 /// Lends a [`Zeroizing<Vec<u8>>`] copy of the secret to `f` and returns
183 /// its result.
184 ///
185 /// The copy is zeroized when `f` drops it.
186 ///
187 /// ```
188 /// use secret_vault_value::SecretValue;
189 ///
190 /// let secret_value = SecretValue::new(vec![1, 2, 3]);
191 /// let sum = secret_value.exposed_in_as_zvec(|bytes| bytes.iter().sum::<u8>());
192 /// assert_eq!(sum, 6);
193 /// ```
194 pub fn exposed_in_as_zvec<T, FN>(&self, f: FN) -> T
195 where
196 FN: Fn(Zeroizing<Vec<u8>>) -> T,
197 {
198 f(Zeroizing::new(self.0.clone()))
199 }
200
201 /// Async form of [`exposed_in_as_str`](Self::exposed_in_as_str): `Z` is
202 /// zeroized once the future returned by `f` completes.
203 ///
204 /// If `f` or its future panics, or the future is dropped before it
205 /// completes (a timeout, a losing `select!` branch, an aborted task), the
206 /// copy is freed without being zeroized. Use
207 /// [`exposed_in_as_zstr_async`](Self::exposed_in_as_zstr_async), which
208 /// zeroizes it in those cases too.
209 ///
210 /// # Panics
211 ///
212 /// Panics if the bytes are not valid UTF-8.
213 ///
214 /// ```
215 /// use secret_vault_value::SecretValue;
216 ///
217 /// async fn password_length(secret_value: &SecretValue) -> usize {
218 /// secret_value
219 /// .exposed_in_as_str_async(|password| async move { (password.len(), password) })
220 /// .await
221 /// }
222 /// # let runtime = tokio::runtime::Runtime::new().unwrap();
223 /// # assert_eq!(runtime.block_on(password_length(&"my-password".into())), 11);
224 /// ```
225 pub async fn exposed_in_as_str_async<T, Z: Zeroize, FN, FI>(&self, f: FN) -> T
226 where
227 FN: Fn(String) -> FI,
228 FI: Future<Output = (T, Z)>,
229 {
230 let decoded_as_string = self.sensitive_value_to_str().unwrap().to_string();
231 let (result, mut zeroizable) = f(decoded_as_string).await;
232 zeroizable.zeroize();
233 result
234 }
235
236 /// Async form of [`exposed_in_as_zstr`](Self::exposed_in_as_zstr): the
237 /// copy is zeroized when the future returned by `f` drops it.
238 ///
239 /// # Panics
240 ///
241 /// Panics if the bytes are not valid UTF-8.
242 ///
243 /// ```
244 /// use secret_vault_value::SecretValue;
245 ///
246 /// async fn password_length(secret_value: &SecretValue) -> usize {
247 /// secret_value
248 /// .exposed_in_as_zstr_async(|password| async move { password.len() })
249 /// .await
250 /// }
251 /// # let runtime = tokio::runtime::Runtime::new().unwrap();
252 /// # assert_eq!(runtime.block_on(password_length(&"my-password".into())), 11);
253 /// ```
254 pub async fn exposed_in_as_zstr_async<T, FN, FI>(&self, f: FN) -> T
255 where
256 FN: Fn(Zeroizing<String>) -> FI,
257 FI: Future<Output = T>,
258 {
259 let decoded_as_string = Zeroizing::new(self.sensitive_value_to_str().unwrap().to_string());
260 f(decoded_as_string).await
261 }
262
263 /// Async form of [`exposed_in_as_vec`](Self::exposed_in_as_vec): `Z` is
264 /// zeroized once the future returned by `f` completes.
265 ///
266 /// If `f` or its future panics, or the future is dropped before it
267 /// completes (a timeout, a losing `select!` branch, an aborted task), the
268 /// copy is freed without being zeroized. Use
269 /// [`exposed_in_as_zvec_async`](Self::exposed_in_as_zvec_async), which
270 /// zeroizes it in those cases too.
271 ///
272 /// ```
273 /// use secret_vault_value::SecretValue;
274 ///
275 /// async fn key_length(secret_value: &SecretValue) -> usize {
276 /// secret_value
277 /// .exposed_in_as_vec_async(|key| async move { (key.len(), key) })
278 /// .await
279 /// }
280 /// # let runtime = tokio::runtime::Runtime::new().unwrap();
281 /// # assert_eq!(runtime.block_on(key_length(&SecretValue::new(vec![1, 2, 3]))), 3);
282 /// ```
283 pub async fn exposed_in_as_vec_async<T, Z: Zeroize, FN, FI>(&self, f: FN) -> T
284 where
285 FN: Fn(Vec<u8>) -> FI,
286 FI: Future<Output = (T, Z)>,
287 {
288 let (result, mut zeroizable) = f(self.0.clone()).await;
289 zeroizable.zeroize();
290 result
291 }
292
293 /// Async form of [`exposed_in_as_zvec`](Self::exposed_in_as_zvec): the
294 /// copy is zeroized when the future returned by `f` drops it.
295 ///
296 /// ```
297 /// use secret_vault_value::SecretValue;
298 ///
299 /// async fn key_length(secret_value: &SecretValue) -> usize {
300 /// secret_value
301 /// .exposed_in_as_zvec_async(|key| async move { key.len() })
302 /// .await
303 /// }
304 /// # let runtime = tokio::runtime::Runtime::new().unwrap();
305 /// # assert_eq!(runtime.block_on(key_length(&SecretValue::new(vec![1, 2, 3]))), 3);
306 /// ```
307 pub async fn exposed_in_as_zvec_async<T, FN, FI>(&self, f: FN) -> T
308 where
309 FN: Fn(Zeroizing<Vec<u8>>) -> FI,
310 FI: Future<Output = T>,
311 {
312 f(Zeroizing::new(self.0.clone())).await
313 }
314}
315
316/// Copies the bytes and zeroizes the source `String`.
317impl From<String> for SecretValue {
318 fn from(mut str: String) -> Self {
319 let result = Self(str.as_bytes().to_vec());
320 str.zeroize();
321 result
322 }
323}
324
325/// Copies the bytes and zeroizes the caller's `String`, leaving it empty.
326impl From<&mut String> for SecretValue {
327 fn from(str: &mut String) -> Self {
328 let result = Self(str.as_bytes().to_vec());
329 str.zeroize();
330 result
331 }
332}
333
334impl From<&Zeroizing<String>> for SecretValue {
335 fn from(str: &Zeroizing<String>) -> Self {
336 Self(str.as_bytes().to_vec())
337 }
338}
339
340impl From<Vec<u8>> for SecretValue {
341 fn from(vec: Vec<u8>) -> Self {
342 Self(vec)
343 }
344}
345
346/// Copies the bytes and zeroizes the caller's `Vec`, leaving it empty.
347impl From<&mut Vec<u8>> for SecretValue {
348 fn from(vec: &mut Vec<u8>) -> Self {
349 let result = Self(vec.clone());
350 vec.zeroize();
351 vec.clear();
352 result
353 }
354}
355
356impl From<&Zeroizing<Vec<u8>>> for SecretValue {
357 fn from(vec: &Zeroizing<Vec<u8>>) -> Self {
358 Self(vec.to_vec())
359 }
360}
361
362impl From<&str> for SecretValue {
363 fn from(str: &str) -> Self {
364 Self(str.as_bytes().to_vec())
365 }
366}
367
368impl Clone for SecretValue {
369 fn clone(&self) -> Self {
370 SecretValue::new(self.ref_sensitive_value().clone())
371 }
372}
373
374impl PartialEq for SecretValue {
375 fn eq(&self, other: &Self) -> bool {
376 self.0.ct_eq(&other.0).into()
377 }
378}
379
380impl Eq for SecretValue {}
381
382impl Hash for SecretValue {
383 fn hash<H: Hasher>(&self, state: &mut H) {
384 self.0.hash(state)
385 }
386}
387
388impl Display for SecretValue {
389 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
390 write!(f, "***")
391 }
392}
393
394impl Debug for SecretValue {
395 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
396 write!(f, "***")
397 }
398}
399
400#[cfg(test)]
401mod test {
402 use super::*;
403 use proptest::prelude::*;
404 use proptest::test_runner::TestRunner;
405 use std::ops::Deref;
406
407 fn generate_secret_value() -> BoxedStrategy<SecretValue> {
408 ("[a-zA-Z0-9]*")
409 .prop_map(|(mock_secret_str)| SecretValue::new(mock_secret_str.as_bytes().to_vec()))
410 .boxed()
411 }
412
413 proptest! {
414 #[test]
415 fn secret_is_not_leaking_in_fmt(mock_secret_value in generate_secret_value()) {
416 assert_eq!(format!("{mock_secret_value}"), "***");
417 assert_eq!(format!("{mock_secret_value:?}"), "***");
418 assert_eq!(format!("{mock_secret_value:#?}"), "***");
419 }
420
421 #[test]
422 fn secret_follows_partial_eq(mock_secret_str in "[a-zA-Z0-9]*") {
423 let mock_secret1 = SecretValue::new(mock_secret_str.as_bytes().to_vec());
424 let mock_secret2 = SecretValue::new(mock_secret_str.as_bytes().to_vec());
425 assert_eq!(mock_secret1, mock_secret2);
426 }
427
428 #[test]
429 fn secret_equality_follows_bytes(first in "[ab]{0,3}", second in "[ab]{0,3}") {
430 let are_equal = SecretValue::from(first.as_str()) == SecretValue::from(second.as_str());
431 assert_eq!(are_equal, first == second);
432 }
433
434 #[test]
435 fn exposed_function_str(mock_secret_value in generate_secret_value()) {
436 let insecure_copy_str =
437 mock_secret_value.exposed_in_as_str(|str| {
438 (str.clone(), str)
439 });
440 assert_eq!(insecure_copy_str.as_str(), mock_secret_value.sensitive_value_to_str().unwrap());
441 }
442
443 #[test]
444 fn exposed_function_zstr(mock_secret_value in generate_secret_value()) {
445 let insecure_copy_str =
446 mock_secret_value.exposed_in_as_zstr(|str| {
447 str.clone()
448 });
449 assert_eq!(insecure_copy_str.as_str(), mock_secret_value.sensitive_value_to_str().unwrap());
450 }
451
452 #[test]
453 fn exposed_function_vec(mock_secret_value in generate_secret_value()) {
454 let insecure_copy_vec =
455 mock_secret_value.exposed_in_as_vec(|vec| {
456 (vec.clone(), vec)
457 });
458 assert_eq!(&insecure_copy_vec, mock_secret_value.ref_sensitive_value());
459 }
460
461
462 #[test]
463 fn exposed_function_zvec(mock_secret_value in generate_secret_value()) {
464 let insecure_copy_vec =
465 mock_secret_value.exposed_in_as_zvec(|vec| {
466 vec.clone()
467 });
468 assert_eq!(insecure_copy_vec.deref(), mock_secret_value.ref_sensitive_value());
469 }
470 }
471
472 #[tokio::test]
473 async fn exposed_function_str_async() {
474 let mut runner = TestRunner::default();
475 let mock_secret = generate_secret_value()
476 .new_tree(&mut runner)
477 .unwrap()
478 .current();
479
480 let insecure_copy_str = mock_secret
481 .exposed_in_as_str_async(|str| async { (str.clone(), str) })
482 .await;
483 assert_eq!(
484 insecure_copy_str.as_str(),
485 mock_secret.sensitive_value_to_str().unwrap()
486 );
487
488 let insecure_copy_str = mock_secret
489 .exposed_in_as_zstr_async(|str| async move { str.clone() })
490 .await;
491 assert_eq!(
492 insecure_copy_str.as_str(),
493 mock_secret.sensitive_value_to_str().unwrap()
494 );
495 }
496
497 #[tokio::test]
498 async fn exposed_function_str_async_closure() {
499 let mut runner = TestRunner::default();
500 let mock_secret = generate_secret_value()
501 .new_tree(&mut runner)
502 .unwrap()
503 .current();
504
505 let test_var_to_capture: String = "test-captured".to_string();
506
507 let insecure_copy_str = mock_secret
508 .exposed_in_as_str_async(|str| async { (format!("{test_var_to_capture}{str}"), str) })
509 .await;
510
511 assert_eq!(
512 insecure_copy_str.as_str(),
513 format!(
514 "{}{}",
515 test_var_to_capture,
516 mock_secret.sensitive_value_to_str().unwrap()
517 )
518 );
519 }
520
521 #[tokio::test]
522 async fn exposed_function_vec_async() {
523 let mut runner = TestRunner::default();
524 let mock_secret = generate_secret_value()
525 .new_tree(&mut runner)
526 .unwrap()
527 .current();
528
529 let insecure_copy_vec = mock_secret
530 .exposed_in_as_vec_async(|vec| async { (vec.clone(), vec) })
531 .await;
532 assert_eq!(&insecure_copy_vec, mock_secret.ref_sensitive_value());
533
534 let insecure_copy_vec = mock_secret
535 .exposed_in_as_zvec_async(|vec| async move { vec.clone() })
536 .await;
537 assert_eq!(insecure_copy_vec.deref(), mock_secret.ref_sensitive_value());
538 }
539
540 #[tokio::test]
541 async fn exposed_function_vec_async_closure() {
542 let mut runner = TestRunner::default();
543 let mock_secret = generate_secret_value()
544 .new_tree(&mut runner)
545 .unwrap()
546 .current();
547
548 let test_var_to_capture: Vec<u8> = "test-captured".to_string().as_bytes().to_vec();
549
550 let insecure_copy_vec = mock_secret
551 .exposed_in_as_vec_async(|vec| async {
552 ([test_var_to_capture.clone(), vec.clone()].concat(), vec)
553 })
554 .await;
555
556 assert_eq!(
557 insecure_copy_vec,
558 [
559 test_var_to_capture.clone(),
560 mock_secret.ref_sensitive_value().to_vec()
561 ]
562 .concat()
563 );
564 }
565}