Skip to main content

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}