Skip to main content

pakery_core/
secret.rs

1//! Zeroizing shared secret type.
2
3use alloc::vec::Vec;
4use subtle::ConstantTimeEq;
5use zeroize::{Zeroize, ZeroizeOnDrop};
6
7/// A shared secret that is automatically zeroized on drop.
8///
9/// Comparisons use constant-time equality to prevent timing side-channels.
10///
11/// # Cloning
12///
13/// This type implements [`Clone`].  Each clone is independently zeroized on
14/// drop, but callers should be mindful that every clone creates an additional
15/// copy of the secret material in memory.  Prefer moving over cloning where
16/// possible.
17#[derive(Clone, Zeroize, ZeroizeOnDrop)]
18pub struct SharedSecret {
19    bytes: Vec<u8>,
20}
21
22impl SharedSecret {
23    /// Create a new `SharedSecret` from raw bytes.
24    pub fn new(bytes: Vec<u8>) -> Self {
25        Self { bytes }
26    }
27
28    /// Access the raw bytes of the shared secret.
29    pub fn as_bytes(&self) -> &[u8] {
30        &self.bytes
31    }
32}
33
34impl ConstantTimeEq for SharedSecret {
35    fn ct_eq(&self, other: &Self) -> subtle::Choice {
36        self.bytes.ct_eq(&other.bytes)
37    }
38}
39
40impl PartialEq for SharedSecret {
41    fn eq(&self, other: &Self) -> bool {
42        // ctgrind: the equality outcome is the caller's public accept/reject
43        // decision; the comparison itself stays constant-time.
44        crate::ct::declassify_choice(self.ct_eq(other))
45    }
46}
47
48impl Eq for SharedSecret {}
49
50impl core::fmt::Debug for SharedSecret {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        f.debug_struct("SharedSecret")
53            .field("bytes", &"[REDACTED]")
54            .finish()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use alloc::vec;
62
63    /// Calling `.zeroize()` on a live value must clear every secret field
64    /// (roadmap item 7: catches a future field added without zeroization).
65    #[test]
66    fn zeroize_clears_all_secret_fields() {
67        let mut secret = SharedSecret::new(vec![0xAA; 32]);
68        secret.zeroize();
69        assert!(secret.bytes.is_empty());
70    }
71
72    /// Equality must hold for identical secrets and fail for differing ones
73    /// (roadmap item 8: a mutant replacing `eq` with `true` survived — the
74    /// negative case was never asserted directly on `SharedSecret`).
75    #[test]
76    fn equality_distinguishes_secrets() {
77        let a = SharedSecret::new(vec![0xAA; 32]);
78        let b = SharedSecret::new(vec![0xAA; 32]);
79        let c = SharedSecret::new(vec![0xBB; 32]);
80        let short = SharedSecret::new(vec![0xAA; 16]);
81        assert_eq!(a, b);
82        assert_ne!(a, c);
83        assert_ne!(a, short);
84    }
85
86    /// Debug output must redact the secret bytes (security convention:
87    /// roadmap item 8 caught the redaction being unasserted).
88    #[test]
89    fn debug_redacts_secret_bytes() {
90        use alloc::format;
91        let rendered = format!("{:?}", SharedSecret::new(vec![0xAB; 4]));
92        assert_eq!(rendered, "SharedSecret { bytes: \"[REDACTED]\" }");
93    }
94}