Skip to main content

secure_gate/sealed/
dynamic.rs

1use alloc::boxed::Box;
2use alloc::string::String;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use super::prekey::{Prekey, SALT_LEN, TAG_LEN, scrubbed};
7use crate::{Dynamic, RevealSecret};
8
9mod private {
10    pub trait Sealed {}
11    impl Sealed for alloc::vec::Vec<u8> {}
12    impl Sealed for alloc::string::String {}
13}
14
15/// The inner types a [`SealedDynamic`] can hold: `Vec<u8>` and `String`. Sealed; it cannot
16/// be implemented outside this crate, because sealing needs a byte view of the secret.
17pub trait SealedPayload: zeroize::Zeroize + private::Sealed + Sized {
18    /// What [`SealedDynamic::with_secret_mut`] hands out: `[u8]` for `Vec<u8>`, `str` for
19    /// `String`. Fixed length, so the closure cannot grow the buffer and leave a
20    /// reallocated copy of the plaintext behind.
21    type Mut: ?Sized;
22
23    #[doc(hidden)]
24    fn bytes(&self) -> &[u8];
25
26    #[doc(hidden)]
27    fn from_plain(plain: Dynamic<Vec<u8>>) -> Dynamic<Self>;
28
29    #[doc(hidden)]
30    fn view_mut(buf: &mut [u8]) -> &mut Self::Mut;
31}
32
33impl SealedPayload for Vec<u8> {
34    type Mut = [u8];
35
36    fn bytes(&self) -> &[u8] {
37        self
38    }
39
40    fn from_plain(plain: Dynamic<Vec<u8>>) -> Dynamic<Self> {
41        plain
42    }
43
44    fn view_mut(buf: &mut [u8]) -> &mut [u8] {
45        buf
46    }
47}
48
49impl SealedPayload for String {
50    type Mut = str;
51
52    fn bytes(&self) -> &[u8] {
53        self.as_bytes()
54    }
55
56    /// Reuses the `Vec`'s buffer; nothing is copied.
57    fn from_plain(plain: Dynamic<Vec<u8>>) -> Dynamic<Self> {
58        match String::from_utf8(plain.into_inner()) {
59            Ok(s) => Dynamic::new(s),
60            Err(not_utf8) => {
61                // Unreachable for plaintext that authenticated; wipe it before panicking anyway.
62                let mut bytes = not_utf8.into_bytes();
63                zeroize::Zeroize::zeroize(&mut bytes);
64                panic!("authenticated plaintext of a sealed String is UTF-8");
65            }
66        }
67    }
68
69    fn view_mut(buf: &mut [u8]) -> &mut str {
70        core::str::from_utf8_mut(buf).expect("authenticated plaintext of a sealed String is UTF-8")
71    }
72}
73
74/// A variable-length secret (`Vec<u8>` or `String`) held encrypted in RAM while idle,
75/// opened for one closure call at a time. The sealed counterpart of [`Dynamic`]. Requires
76/// the `sealed` feature.
77///
78/// The ciphertext lives in an exact-size `Box<[u8]>`. Each access decrypts into an
79/// exact-capacity [`Dynamic`] that its `Drop` wipes. As with [`SealedFixed`](crate::SealedFixed)
80/// there is no `expose_secret` and no [`RevealSecret`] impl.
81///
82/// ```rust
83/// use secure_gate::{Dynamic, Prekey, SealedDynamic};
84///
85/// let prekey = Prekey::new();
86/// let mut pw = SealedDynamic::seal(Dynamic::new(String::from("hunter2")), &prekey);
87/// pw.with_secret(|s: &String| assert_eq!(s, "hunter2"));
88/// pw.with_secret_mut(|s: &mut str| s.make_ascii_uppercase());
89/// pw.with_secret(|s| assert_eq!(s, "HUNTER2"));
90/// ```
91///
92/// # Panics
93///
94/// Every access panics if the sealed bytes fail to authenticate. The temporary is wiped and
95/// the stack below the call scrubbed before the panic continues.
96#[must_use = "holds secret material; bind it or chain a method call"]
97pub struct SealedDynamic<T: SealedPayload> {
98    ct: Box<[u8]>,
99    tag: [u8; TAG_LEN],
100    salt: [u8; SALT_LEN],
101    prekey: Arc<Prekey>,
102    _payload: core::marker::PhantomData<fn() -> T>,
103}
104
105impl<T: SealedPayload> SealedDynamic<T> {
106    /// Seals `secret` under `prekey`. The plain wrapper is consumed and wiped on drop.
107    pub fn seal(secret: Dynamic<T>, prekey: &Arc<Prekey>) -> Self {
108        let salt = Prekey::fresh_salt();
109        // Held in a wiping buffer until it is ciphertext: if the encrypt panics, the
110        // plaintext copy is wiped as it unwinds instead of going back to the allocator.
111        let mut buf =
112            zeroize::Zeroizing::new(secret.with_secret(|plain| Box::<[u8]>::from(plain.bytes())));
113        let tag = scrubbed(|| prekey.seal_in_place(&salt, &mut buf));
114        let ct = core::mem::take(&mut *buf);
115        Self {
116            ct,
117            tag,
118            salt,
119            prekey: Arc::clone(prekey),
120            _payload: core::marker::PhantomData,
121        }
122    }
123
124    /// Decrypts into a temporary for the length of `f`, then wipes the temporary.
125    pub fn with_secret<F, R>(&self, f: F) -> R
126    where
127        F: FnOnce(&T) -> R,
128    {
129        scrubbed(|| {
130            let plain = T::from_plain(self.open());
131            plain.with_secret(f)
132        })
133    }
134
135    /// Decrypts into a temporary, lets `f` change it in place, then seals the result under
136    /// a fresh salt and wipes the temporary.
137    ///
138    /// `f` gets `&mut [u8]` or `&mut str`, never `&mut Vec` or `&mut String`: the length is
139    /// fixed, so the buffer can never reallocate and abandon an unwiped copy of the
140    /// plaintext. To change the length, [`unseal`](Self::unseal), edit, and seal again.
141    ///
142    /// If `f` panics, the temporary is still wiped, the stack scrubbed, and the sealed value
143    /// left as it was.
144    pub fn with_secret_mut<F, R>(&mut self, f: F) -> R
145    where
146        F: FnOnce(&mut T::Mut) -> R,
147    {
148        scrubbed(|| {
149            let mut plain = self.open();
150            let result =
151                crate::RevealSecretMut::with_secret_mut(&mut plain, |buf| f(T::view_mut(buf)));
152            let salt = Prekey::fresh_salt();
153            plain.with_secret(|p| self.ct.copy_from_slice(p));
154            self.tag = self.prekey.seal_in_place(&salt, &mut self.ct);
155            self.salt = salt;
156            result
157        })
158    }
159
160    /// Opens the secret back into a plain [`Dynamic`]. Protection from sealing ends here;
161    /// the returned wrapper still wipes on drop.
162    pub fn unseal(self) -> Dynamic<T> {
163        scrubbed(|| T::from_plain(self.open()))
164    }
165
166    /// The secret's length in bytes. Length can itself be sensitive for passwords; it is
167    /// not printed by `Debug`.
168    #[inline]
169    pub fn len(&self) -> usize {
170        self.ct.len()
171    }
172
173    /// `true` if the secret is empty.
174    #[inline]
175    pub fn is_empty(&self) -> bool {
176        self.ct.is_empty()
177    }
178
179    fn open(&self) -> Dynamic<Vec<u8>> {
180        Dynamic::<Vec<u8>>::new_with(self.ct.len(), |buf| self.open_into(buf))
181    }
182
183    fn open_into(&self, buf: &mut [u8]) {
184        buf.copy_from_slice(&self.ct);
185        self.prekey.open_in_place(&self.salt, &self.tag, buf);
186    }
187}
188
189impl<T: SealedPayload> crate::scoped::RevealScoped for SealedDynamic<T> {
190    type Inner = T;
191
192    #[inline]
193    fn with_secret<F, R>(&self, f: F) -> R
194    where
195        F: FnOnce(&T) -> R,
196    {
197        SealedDynamic::with_secret(self, f)
198    }
199}
200
201/// Constant-time equality: opens both sides and compares the plaintext with `subtle`.
202/// Unequal lengths compare unequal without opening either side.
203#[cfg(feature = "ct-eq")]
204impl<T: SealedPayload> crate::ConstantTimeEq for SealedDynamic<T> {
205    fn ct_eq(&self, other: &Self) -> bool {
206        if self.len() != other.len() {
207            return false;
208        }
209        self.with_secret(|a| {
210            other.with_secret(|b| crate::ConstantTimeEq::ct_eq(a.bytes(), b.bytes()))
211        })
212    }
213}
214
215/// Always prints `[REDACTED]`: no salt, no tag, no length.
216impl<T: SealedPayload> core::fmt::Debug for SealedDynamic<T> {
217    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218        f.write_str("[REDACTED]")
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::sealed::prekey::tests::quota;
226
227    #[test]
228    fn the_ciphertext_is_not_the_plaintext_and_differs_per_seal() {
229        let _q = quota();
230        let prekey = Prekey::new();
231        let a = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(alloc::vec![7u8; 40]), &prekey);
232        let b = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(alloc::vec![7u8; 40]), &prekey);
233        assert_ne!(&*a.ct, &[7u8; 40][..]);
234        assert_ne!(a.salt, b.salt);
235        assert_ne!(a.ct, b.ct);
236    }
237
238    #[test]
239    fn with_secret_mut_reseals_under_a_fresh_salt() {
240        let _q = quota();
241        let prekey = Prekey::new();
242        let mut s = SealedDynamic::seal(Dynamic::<String>::new(String::from("abc")), &prekey);
243        let (salt, ct) = (s.salt, s.ct.clone());
244        s.with_secret_mut(|p: &mut str| p.make_ascii_uppercase());
245        assert_ne!(s.salt, salt);
246        assert_ne!(s.ct, ct);
247        s.with_secret(|p| assert_eq!(p, "ABC"));
248    }
249
250    #[test]
251    #[should_panic(expected = "sealed memory was modified or corrupted")]
252    fn a_flipped_tag_bit_panics() {
253        let _q = quota();
254        let prekey = Prekey::new();
255        let mut s = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(alloc::vec![1u8; 5]), &prekey);
256        s.tag[15] ^= 0x80;
257        s.with_secret(|_| ());
258    }
259}