Skip to main content

quantica/
secret.rs

1//! Zeroize-on-Drop containers for secret key material.
2//!
3//! `quantica` exposes secret keys, signing keys and shared secrets
4//! through wrapper types that automatically wipe their backing memory
5//! when dropped, using the constant-time zeroization primitive from
6//! the [`silentops`] crate.
7//!
8//! Two building blocks live here:
9//!
10//! - `SecretBytes` — heap-allocated, variable-length zeroizing
11//!   container, used as the storage for `DecapsulationKey<P>`,
12//!   `SigningKey<P>`, and similar types whose length depends on the
13//!   parameter set chosen at runtime.
14//! - `SecretArray` — stack-allocated, fixed-size zeroizing
15//!   container, used for the 32-byte ML-KEM shared secret.
16//!
17//! Both types implement [`Deref<Target = [u8]>`](core::ops::Deref) so
18//! callers can pass them transparently to any function expecting a
19//! `&[u8]`.
20
21use alloc::vec::Vec;
22use core::fmt;
23use core::ops::{Deref, DerefMut};
24
25/// Heap-allocated, variable-length container that wipes its contents
26/// on [`Drop`] using `silentops::ct_zeroize`.
27///
28/// Used as the storage for the secret-half of every key pair in the
29/// crate. The wipe is performed via `write_volatile` + a compiler
30/// fence so the optimizer is not allowed to elide it.
31#[derive(Clone)]
32pub struct SecretBytes {
33    bytes: Vec<u8>,
34}
35
36impl SecretBytes {
37    /// Build a [`SecretBytes`] from an existing `Vec<u8>`.
38    ///
39    /// The original vector is moved into the wrapper; no copy occurs.
40    pub fn from_vec(bytes: Vec<u8>) -> Self {
41        Self { bytes }
42    }
43
44    /// Build a [`SecretBytes`] by copying the contents of `data`.
45    pub fn from_slice(data: &[u8]) -> Self {
46        Self { bytes: data.to_vec() }
47    }
48
49    /// Borrow the secret as a byte slice.
50    pub fn as_bytes(&self) -> &[u8] {
51        &self.bytes
52    }
53
54    /// Length in bytes.
55    pub fn len(&self) -> usize {
56        self.bytes.len()
57    }
58
59    /// Whether the container is empty.
60    pub fn is_empty(&self) -> bool {
61        self.bytes.is_empty()
62    }
63}
64
65impl Drop for SecretBytes {
66    fn drop(&mut self) {
67        silentops::ct_zeroize(&mut self.bytes);
68    }
69}
70
71impl Deref for SecretBytes {
72    type Target = [u8];
73    fn deref(&self) -> &[u8] {
74        &self.bytes
75    }
76}
77
78impl DerefMut for SecretBytes {
79    fn deref_mut(&mut self) -> &mut [u8] {
80        &mut self.bytes
81    }
82}
83
84impl AsRef<[u8]> for SecretBytes {
85    fn as_ref(&self) -> &[u8] {
86        &self.bytes
87    }
88}
89
90/// Redacted [`Debug`] impl: prints the type and length but never the
91/// secret bytes themselves, so accidentally logging a key won't leak
92/// it.
93impl fmt::Debug for SecretBytes {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "SecretBytes(<redacted; len={}>)", self.bytes.len())
96    }
97}
98
99/// Stack-allocated, fixed-size byte array that wipes itself on
100/// [`Drop`].
101///
102/// Used for shared secrets and other small secret values whose
103/// length is known at compile time. The wipe goes through
104/// `silentops::ct_zeroize`, which is `write_volatile` + a compiler
105/// fence.
106#[derive(Clone)]
107pub struct SecretArray<const N: usize> {
108    bytes: [u8; N],
109}
110
111impl<const N: usize> SecretArray<N> {
112    /// Wrap a raw `[u8; N]` array.
113    pub fn new(bytes: [u8; N]) -> Self {
114        Self { bytes }
115    }
116
117    /// Borrow the secret as a byte slice.
118    pub fn as_bytes(&self) -> &[u8] {
119        &self.bytes
120    }
121
122    /// Borrow the secret as a fixed-size array reference.
123    pub fn as_array(&self) -> &[u8; N] {
124        &self.bytes
125    }
126
127    /// Length in bytes (always `N`).
128    pub fn len(&self) -> usize {
129        N
130    }
131
132    /// Constant-time equality with another secret of the same length.
133    ///
134    /// Wraps `silentops::ct_eq` so the comparison itself does not leak
135    /// timing information about which byte first differed.
136    pub fn ct_eq(&self, other: &Self) -> bool {
137        silentops::ct_eq(&self.bytes, &other.bytes) == 1
138    }
139}
140
141impl<const N: usize> Drop for SecretArray<N> {
142    fn drop(&mut self) {
143        silentops::ct_zeroize(&mut self.bytes);
144    }
145}
146
147impl<const N: usize> Deref for SecretArray<N> {
148    type Target = [u8];
149    fn deref(&self) -> &[u8] {
150        &self.bytes
151    }
152}
153
154impl<const N: usize> AsRef<[u8]> for SecretArray<N> {
155    fn as_ref(&self) -> &[u8] {
156        &self.bytes
157    }
158}
159
160impl<const N: usize> PartialEq for SecretArray<N> {
161    /// Constant-time equality (delegates to [`SecretArray::ct_eq`]).
162    fn eq(&self, other: &Self) -> bool {
163        self.ct_eq(other)
164    }
165}
166impl<const N: usize> Eq for SecretArray<N> {}
167
168/// Redacted [`Debug`] impl: prints the type and length but never the
169/// secret bytes, so accidentally logging a shared secret won't leak it.
170impl<const N: usize> fmt::Debug for SecretArray<N> {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(f, "SecretArray<{}>(<redacted>)", N)
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn secret_bytes_zeroes_on_drop() {
182        // Use the inner Vec's pointer to peek at the heap region after
183        // the wrapper is dropped. We can't dereference it (UAF in
184        // practice) but we can construct a fresh allocation likely to
185        // land on the same spot, and confirm new contents differ. The
186        // real guarantee is that ct_zeroize ran, which we test more
187        // directly via the public API on the wrapper before Drop.
188        let mut s = SecretBytes::from_slice(&[0xAA; 64]);
189        for &b in s.as_bytes() {
190            assert_eq!(b, 0xAA);
191        }
192        // Mutate via DerefMut, then re-read.
193        s.fill(0x55);
194        for &b in s.as_bytes() {
195            assert_eq!(b, 0x55);
196        }
197        // Drop happens at end of scope; the test simply confirms
198        // the API surface compiles and behaves linearly.
199    }
200
201    #[test]
202    fn secret_array_ct_eq() {
203        let a = SecretArray::<8>::new([1, 2, 3, 4, 5, 6, 7, 8]);
204        let b = SecretArray::<8>::new([1, 2, 3, 4, 5, 6, 7, 8]);
205        let c = SecretArray::<8>::new([1, 2, 3, 4, 5, 6, 7, 9]);
206        assert!(a == b);
207        assert!(a != c);
208        assert!(a.ct_eq(&b));
209        assert!(!a.ct_eq(&c));
210    }
211}