commonware_cryptography/
secret.rs

1//! A wrapper type for secret values that prevents accidental leakage.
2//!
3//! `Secret<T>` provides the following guarantees:
4//! - Debug and Display always show `[REDACTED]` instead of the actual value
5//! - The inner value is zeroized on drop
6//! - Access to the inner value requires an explicit `expose()` call
7//! - Comparisons use constant-time operations to prevent timing attacks
8//!
9//! # Type Constraints
10//!
11//! **Important**: `Secret<T>` is designed for flat data types without pointers
12//! (e.g. `[u8; N]`). It does NOT provide full protection for types with
13//! indirection. Types like `Vec<T>`, `String`, or `Box<T>` will only have their
14//! metadata (pointer, length, capacity) zeroized, the referenced data remains
15//! intact. Do not use `Secret` with types that contain pointers.
16
17use core::{
18    fmt::{Debug, Display, Formatter},
19    mem::ManuallyDrop,
20};
21use ctutils::CtEq;
22use zeroize::{Zeroize, ZeroizeOnDrop};
23
24/// Zeroize memory at the given pointer using volatile writes.
25///
26/// # Safety
27///
28/// `ptr` must point to allocated, writable memory of at least `size_of::<T>()` bytes.
29#[inline]
30unsafe fn zeroize_ptr<T>(ptr: *mut T) {
31    let slice = core::slice::from_raw_parts_mut(ptr as *mut u8, core::mem::size_of::<T>());
32    slice.zeroize();
33}
34
35/// A wrapper for secret values that prevents accidental leakage.
36///
37/// - Debug and Display show `[REDACTED]`
38/// - Zeroized on drop
39/// - Access requires explicit `expose()` call
40///
41/// # Type Constraints
42///
43/// Only use with flat data types that have no pointers (e.g. `[u8; N]`).
44/// See [module-level documentation](self) for details.
45pub struct Secret<T>(ManuallyDrop<T>);
46
47impl<T> Secret<T> {
48    /// Creates a new `Secret` wrapping the given value.
49    #[inline]
50    pub const fn new(value: T) -> Self {
51        Self(ManuallyDrop::new(value))
52    }
53
54    /// Exposes the secret value for read-only access within a closure.
55    ///
56    /// # Note
57    ///
58    /// The closure uses a higher-ranked trait bound (`for<'a>`) to prevent
59    /// the returned value from containing references to the secret data.
60    /// This ensures the reference cannot escape the closure scope. However,
61    /// this does not prevent copying or cloning the secret value within
62    /// the closure (e.g., `secret.expose(|s| s.clone())`). Callers should
63    /// avoid leaking secrets through such patterns.
64    ///
65    /// Additionally, any temporaries derived from the secret (e.g.
66    /// `s.as_slice()`) may leave secret data on the stack that will not be
67    /// automatically zeroized. Callers should wrap such temporaries in
68    /// [`zeroize::Zeroizing`] if they contain sensitive data.
69    #[inline]
70    pub fn expose<R>(&self, f: impl for<'a> FnOnce(&'a T) -> R) -> R {
71        f(&self.0)
72    }
73
74    /// Consumes the [Secret] and returns the inner value, zeroizing the original
75    /// memory location.
76    ///
77    /// Use this when you need to transfer ownership of the secret value (e.g.,
78    /// for APIs that consume the value).
79    #[inline]
80    pub fn expose_unwrap(mut self) -> T {
81        let ptr = &raw mut *self.0;
82        // SAFETY:
83        // Pointer obtained while self.0 is still initialized,
84        // self.0 is initialized and we have exclusive access
85        let value = unsafe { ManuallyDrop::take(&mut self.0) };
86
87        // Prevent Secret::drop from running (would double-zeroize or double-free on panic)
88        core::mem::forget(self);
89
90        // SAFETY: uses raw pointer (not reference) to zero memory after drop
91        unsafe { zeroize_ptr(ptr) };
92
93        value
94    }
95}
96
97impl<T> Drop for Secret<T> {
98    fn drop(&mut self) {
99        let ptr = &raw mut *self.0;
100        // SAFETY:
101        // - Pointer obtained while self.0 is still initialized
102        // - ManuallyDrop::drop: self.0 is initialized and we have exclusive access
103        // - zeroize_ptr: uses raw pointer (not reference) to zero memory after drop
104        unsafe {
105            ManuallyDrop::drop(&mut self.0);
106            zeroize_ptr(ptr);
107        }
108    }
109}
110
111impl<T> Debug for Secret<T> {
112    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
113        f.write_str("Secret([REDACTED])")
114    }
115}
116
117impl<T> Display for Secret<T> {
118    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
119        f.write_str("[REDACTED]")
120    }
121}
122
123impl<T> ZeroizeOnDrop for Secret<T> {}
124
125impl<T: Clone> Clone for Secret<T> {
126    fn clone(&self) -> Self {
127        self.expose(|v| Self::new(v.clone()))
128    }
129}
130
131impl<T: CtEq> PartialEq for Secret<T> {
132    fn eq(&self, other: &Self) -> bool {
133        self.expose(|a| other.expose(|b| a.ct_eq(b).into()))
134    }
135}
136
137impl<T: CtEq> Eq for Secret<T> {}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_debug_redacted() {
145        let secret = Secret::new([1u8, 2, 3, 4]);
146        assert_eq!(format!("{:?}", secret), "Secret([REDACTED])");
147    }
148
149    #[test]
150    fn test_display_redacted() {
151        let secret = Secret::new([1u8, 2, 3, 4]);
152        assert_eq!(format!("{}", secret), "[REDACTED]");
153    }
154
155    #[test]
156    fn test_expose() {
157        let secret = Secret::new([1u8, 2, 3, 4]);
158        secret.expose(|v| {
159            assert_eq!(v, &[1u8, 2, 3, 4]);
160        });
161    }
162
163    #[test]
164    fn test_expose_unwrap() {
165        let secret = Secret::new([1u8, 2, 3, 4]);
166        let value = secret.expose_unwrap();
167        assert_eq!(value, [1u8, 2, 3, 4]);
168    }
169
170    #[test]
171    fn test_clone() {
172        let secret = Secret::new([1u8, 2, 3, 4]);
173        let cloned = secret.clone();
174        secret.expose(|a| {
175            cloned.expose(|b| {
176                assert_eq!(a, b);
177            });
178        });
179    }
180
181    #[test]
182    fn test_equality() {
183        let s1 = Secret::new([1u8, 2, 3, 4]);
184        let s2 = Secret::new([1u8, 2, 3, 4]);
185        let s3 = Secret::new([5u8, 6, 7, 8]);
186        assert_eq!(s1, s2);
187        assert_ne!(s1, s3);
188    }
189
190    #[test]
191    fn test_multiple_expose() {
192        let secret = Secret::new([42u8; 32]);
193
194        // First expose
195        secret.expose(|v| {
196            assert_eq!(v[0], 42);
197        });
198
199        // Second expose
200        secret.expose(|v| {
201            assert_eq!(v[31], 42);
202        });
203    }
204}