Skip to main content

xmtp_cryptography/
secret.rs

1//! Secret bytes and the zeroize helpers.
2//!
3//! This module is the one import path for zeroization in libxmtp:
4//!
5//! - [`Secret`] owns secret bytes in a struct field, a return value or a
6//!   long-lived binding. It is wiped on drop and its `Debug` shows only the
7//!   length.
8//! - [`Zeroizing`] wraps a transient buffer (a serialization output, a
9//!   `to_vec()`, an FFI argument) at the point where it is created. Its
10//!   `Debug` prints the inner value, so never format one.
11//! - [`Zeroize`] and [`ZeroizeOnDrop`] are for hand-written secret holders.
12//!
13//! With the `test-utils` feature, `heap_canary` provides the test allocator
14//! that proves a secret is wiped before its heap block is freed.
15
16use std::fmt;
17
18use serde::de::{SeqAccess, Visitor};
19use subtle::ConstantTimeEq;
20pub use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
21
22#[cfg(all(feature = "test-utils", unix))]
23pub mod heap_canary;
24
25/// Capacity reserved before a [`Secret`] is deserialized.
26///
27/// Every key libxmtp holds as a `Secret` fits, so decoding one never reallocates
28/// and frees a partial copy.
29const DESERIALIZE_CAPACITY: usize = 64;
30
31/// Owned secret bytes. Wiped on drop; `Debug` shows only the length.
32///
33/// Serializes as a byte string (`serialize_bytes`, CBOR major type 2), and
34/// deserializes one into a wiping buffer with a single copy. A human-readable
35/// format, such as JSON, has no byte strings: there it reads a sequence of
36/// `u8` and nothing else, so a JSON string is an error, not its text's bytes.
37/// A type whose serde shape stores its secret as a sequence of `u8`, as
38/// OpenMLS's `SignatureKeyPair` does, reads it with [`deserialize_seq`].
39/// Equality is constant time.
40#[derive(Clone)]
41pub struct Secret(Zeroizing<Vec<u8>>);
42
43impl Secret {
44    /// Take ownership of `bytes`. The buffer is wiped when the `Secret` drops.
45    pub fn new(bytes: Vec<u8>) -> Self {
46        Self(Zeroizing::new(bytes))
47    }
48
49    /// Borrow the secret bytes.
50    pub fn as_slice(&self) -> &[u8] {
51        self.0.as_slice()
52    }
53}
54
55impl From<Vec<u8>> for Secret {
56    fn from(bytes: Vec<u8>) -> Self {
57        Self::new(bytes)
58    }
59}
60
61impl From<Secret> for Zeroizing<Vec<u8>> {
62    fn from(secret: Secret) -> Self {
63        secret.0
64    }
65}
66
67impl fmt::Debug for Secret {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "Secret(<{} bytes>)", self.0.len())
70    }
71}
72
73impl PartialEq for Secret {
74    fn eq(&self, other: &Self) -> bool {
75        self.as_slice().ct_eq(other.as_slice()).into()
76    }
77}
78
79impl Eq for Secret {}
80
81impl ZeroizeOnDrop for Secret {}
82
83/// A secret is stored in the MLS key store as one CBOR byte string, and
84/// reading it through `Secret` copies the bytes once into a wiping buffer.
85impl openmls_traits::storage::Entity<{ openmls_traits::storage::CURRENT_VERSION }> for Secret {}
86
87impl serde::Serialize for Secret {
88    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
89        serializer.serialize_bytes(self.as_slice())
90    }
91}
92
93impl<'de> serde::Deserialize<'de> for Secret {
94    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
95        if deserializer.is_human_readable() {
96            deserializer.deserialize_seq(SecretVisitor)
97        } else {
98            deserializer.deserialize_bytes(SecretVisitor)
99        }
100    }
101}
102
103/// Read a [`Secret`] stored as a sequence of `u8`, the serde shape of a
104/// `Vec<u8>`, into a wiping buffer. For a type whose shape another crate
105/// owns, such as OpenMLS's `SignatureKeyPair`.
106pub fn deserialize_seq<'de, D: serde::Deserializer<'de>>(
107    deserializer: D,
108) -> Result<Secret, D::Error> {
109    deserializer.deserialize_seq(SecretVisitor)
110}
111
112/// Reads secret bytes into a wiping buffer. Borrowed bytes are copied once
113/// into a buffer of their exact size; an owned buffer moves in without a
114/// copy; a sequence (from a human-readable format, or from
115/// [`deserialize_seq`]) is pushed into a buffer that grows only through
116/// wiped copies.
117///
118/// A decode that fails partway frees nothing unwiped behind the client
119/// database's pre-scan (`xmtp_db`'s `value_from_slice`), which rejects a cut
120/// or indefinite-length item before any decode. Read directly, cbor4ii copies
121/// what it has of a cut byte string into a buffer that is not wiped before
122/// it fails.
123struct SecretVisitor;
124
125impl<'de> Visitor<'de> for SecretVisitor {
126    type Value = Secret;
127
128    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.write_str("secret bytes")
130    }
131
132    fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Secret, E> {
133        let mut secret = Zeroizing::new(Vec::with_capacity(bytes.len()));
134        secret.extend_from_slice(bytes);
135        Ok(Secret(secret))
136    }
137
138    fn visit_byte_buf<E: serde::de::Error>(self, bytes: Vec<u8>) -> Result<Secret, E> {
139        Ok(Secret(Zeroizing::new(bytes)))
140    }
141
142    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Secret, A::Error> {
143        let mut bytes = Zeroizing::new(Vec::with_capacity(DESERIALIZE_CAPACITY));
144        while let Some(byte) = seq.next_element::<u8>()? {
145            push_zeroizing(&mut bytes, byte);
146        }
147        Ok(Secret(bytes))
148    }
149}
150
151/// Push `byte`, growing through a new zeroizing buffer when `bytes` is full.
152///
153/// `Vec::push` would reallocate and free the old block without wiping it.
154fn push_zeroizing(bytes: &mut Zeroizing<Vec<u8>>, byte: u8) {
155    if bytes.len() == bytes.capacity() {
156        let mut grown = Zeroizing::new(Vec::with_capacity(bytes.capacity().max(1) * 2));
157        grown.extend_from_slice(bytes);
158        // The old buffer is wiped when `grown` replaces it.
159        *bytes = grown;
160    }
161    bytes.push(byte);
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    const KEY: [u8; 32] = [
169        0x8f, 0x13, 0x2a, 0x55, 0xc0, 0x01, 0x7e, 0x9d, 0x42, 0xbb, 0x06, 0xe1, 0x3c, 0x77, 0xa4,
170        0x18, 0xd2, 0x5b, 0x90, 0x6f, 0x24, 0xe8, 0x31, 0xcd, 0x0a, 0x99, 0x4e, 0xf7, 0x63, 0xb5,
171        0x2c, 0x81,
172    ];
173
174    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
175    #[cfg_attr(not(target_arch = "wasm32"), test)]
176    fn debug_shows_only_the_length() {
177        let debug = format!("{:?}", Secret::new(KEY.to_vec()));
178        assert_eq!(debug, "Secret(<32 bytes>)");
179        assert!(!debug.contains(&hex::encode(KEY)));
180        assert!(!debug.contains(&format!("{:?}", KEY.as_slice())));
181    }
182
183    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
184    #[cfg_attr(not(target_arch = "wasm32"), test)]
185    fn serde_is_a_byte_string() {
186        for len in [0, 1, 23, 24, 32, 64, 65, 200] {
187            let bytes: Vec<u8> = (0..len).map(|i| i as u8).collect();
188            let secret = Secret::new(bytes.clone());
189
190            // CBOR, the client database's codec: a byte string, so the raw
191            // bytes follow the header.
192            let cbor_secret = cbor4ii::serde::to_vec(Vec::new(), &secret).unwrap();
193            let header = match u8::try_from(len).unwrap() {
194                short @ 0..=23 => vec![0x40 | short],
195                long => vec![0x58, long],
196            };
197            assert_eq!(cbor_secret, [header.as_slice(), &bytes].concat());
198            let decoded: Secret = cbor4ii::serde::from_slice(&cbor_secret).unwrap();
199            assert_eq!(decoded.as_slice(), bytes.as_slice());
200
201            // A secret in a type whose shape is a `Vec<u8>` reads with
202            // `deserialize_seq`.
203            let cbor_vec = cbor4ii::serde::to_vec(Vec::new(), &bytes).unwrap();
204            let mut reader = cbor4ii::core::utils::SliceReader::new(&cbor_vec);
205            let decoded =
206                deserialize_seq(&mut cbor4ii::serde::Deserializer::new(&mut reader)).unwrap();
207            assert_eq!(decoded.as_slice(), bytes.as_slice());
208
209            // JSON has no byte strings: an array of numbers, like a `Vec<u8>`.
210            let json_secret = serde_json::to_vec(&secret).unwrap();
211            assert_eq!(json_secret, serde_json::to_vec(&bytes).unwrap());
212            let decoded: Secret = serde_json::from_slice(&json_secret).unwrap();
213            assert_eq!(decoded.as_slice(), bytes.as_slice());
214        }
215    }
216
217    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
218    #[cfg_attr(not(target_arch = "wasm32"), test)]
219    fn serde_rejects_what_is_not_secret_bytes() {
220        // JSON has no byte strings, and a string is not secret bytes:
221        // serde_json would hand its text's bytes to a byte visitor.
222        for json in [
223            "\"0x01\"", "\"AQI=\"", "[256]", "[-1]", "[1,", "{}", "null", "7",
224        ] {
225            assert!(serde_json::from_str::<Secret>(json).is_err(), "{json}");
226        }
227        // CBOR: a byte string longer than the input, the `Vec<u8>` form (an
228        // array of integers), and a text string.
229        let mut truncated = cbor4ii::serde::to_vec(Vec::new(), &Secret::new(KEY.to_vec())).unwrap();
230        truncated.truncate(truncated.len() - 1);
231        let integers = cbor4ii::serde::to_vec(Vec::new(), &KEY.to_vec()).unwrap();
232        for cbor in [&truncated[..], &integers, &[0x62, b'a', b'b']] {
233            assert!(
234                cbor4ii::serde::from_slice::<Secret>(cbor).is_err(),
235                "{cbor:02x?}"
236            );
237        }
238    }
239
240    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
241    #[cfg_attr(not(target_arch = "wasm32"), test)]
242    fn equality_compares_bytes() {
243        assert_eq!(Secret::new(KEY.to_vec()), Secret::from(KEY.to_vec()));
244        assert_ne!(Secret::new(KEY.to_vec()), Secret::new(KEY[..31].to_vec()));
245        let mut other = KEY;
246        other[31] ^= 1;
247        assert_ne!(Secret::new(KEY.to_vec()), Secret::new(other.to_vec()));
248    }
249}