Skip to main content

quantica/
secret.rs

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