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            self.tag = plain.with_secret(|p| self.prekey.seal_into(&salt, p, &mut self.ct));
154            self.salt = salt;
155            result
156        })
157    }
158
159    /// Opens the secret back into a plain [`Dynamic`]. Protection from sealing ends here;
160    /// the returned wrapper still wipes on drop.
161    pub fn unseal(self) -> Dynamic<T> {
162        scrubbed(|| T::from_plain(self.open()))
163    }
164
165    /// The secret's length in bytes. Length can itself be sensitive for passwords; it is
166    /// not printed by `Debug`.
167    #[inline]
168    pub fn len(&self) -> usize {
169        self.ct.len()
170    }
171
172    /// `true` if the secret is empty.
173    #[inline]
174    pub fn is_empty(&self) -> bool {
175        self.ct.is_empty()
176    }
177
178    fn open(&self) -> Dynamic<Vec<u8>> {
179        Dynamic::<Vec<u8>>::new_with(self.ct.len(), |buf| self.open_into(buf))
180    }
181
182    fn open_into(&self, buf: &mut [u8]) {
183        buf.copy_from_slice(&self.ct);
184        self.prekey.open_in_place(&self.salt, &self.tag, buf);
185    }
186}
187
188impl<T: SealedPayload> crate::scoped::RevealScoped for SealedDynamic<T> {
189    type Inner = T;
190
191    #[inline]
192    fn with_secret<F, R>(&self, f: F) -> R
193    where
194        F: FnOnce(&T) -> R,
195    {
196        SealedDynamic::with_secret(self, f)
197    }
198}
199
200/// Constant-time equality: opens both sides and compares the plaintext with `subtle`.
201/// Unequal lengths compare unequal without opening either side.
202#[cfg(feature = "ct-eq")]
203impl<T: SealedPayload> crate::ConstantTimeEq for SealedDynamic<T> {
204    fn ct_eq(&self, other: &Self) -> bool {
205        if self.len() != other.len() {
206            return false;
207        }
208        self.with_secret(|a| {
209            other.with_secret(|b| crate::ConstantTimeEq::ct_eq(a.bytes(), b.bytes()))
210        })
211    }
212}
213
214/// Always prints `[REDACTED]`: no salt, no tag, no length.
215impl<T: SealedPayload> core::fmt::Debug for SealedDynamic<T> {
216    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
217        f.write_str("[REDACTED]")
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::sealed::prekey::tests::quota;
225
226    #[test]
227    fn the_ciphertext_is_not_the_plaintext_and_differs_per_seal() {
228        let _q = quota();
229        let prekey = Prekey::new();
230        let a = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(alloc::vec![7u8; 40]), &prekey);
231        let b = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(alloc::vec![7u8; 40]), &prekey);
232        assert_ne!(&*a.ct, &[7u8; 40][..]);
233        assert_ne!(a.salt, b.salt);
234        assert_ne!(a.ct, b.ct);
235    }
236
237    /// As for `SealedFixed`: a re-seal whose key derivation fails leaves the old
238    /// ciphertext, never the new plaintext.
239    #[test]
240    fn a_failed_reseal_leaves_ciphertext_not_plaintext() {
241        let _q = quota();
242        let prekey = Prekey::new();
243        let mut s = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(vec![0x42u8; 40]), &prekey);
244        let before = s.ct.clone();
245        let r = std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {
246            s.with_secret_mut(|b| {
247                b.fill(0x99);
248                crate::sealed::prekey::tests::fail_next_derivation();
249            });
250        }));
251        assert!(r.is_err());
252        assert!(
253            s.ct.iter().any(|&x| x != 0x99),
254            "the new plaintext was left in the ciphertext"
255        );
256        assert_eq!(s.ct, before);
257        s.with_secret(|b| assert_eq!(b.as_slice(), &[0x42u8; 40]));
258    }
259
260    #[test]
261    fn with_secret_mut_reseals_under_a_fresh_salt() {
262        let _q = quota();
263        let prekey = Prekey::new();
264        let mut s = SealedDynamic::seal(Dynamic::<String>::new(String::from("abc")), &prekey);
265        let (salt, ct) = (s.salt, s.ct.clone());
266        s.with_secret_mut(|p: &mut str| p.make_ascii_uppercase());
267        assert_ne!(s.salt, salt);
268        assert_ne!(s.ct, ct);
269        s.with_secret(|p| assert_eq!(p, "ABC"));
270    }
271
272    #[test]
273    #[should_panic(expected = "sealed memory was modified or corrupted")]
274    fn a_flipped_tag_bit_panics() {
275        let _q = quota();
276        let prekey = Prekey::new();
277        let mut s = SealedDynamic::seal(Dynamic::<Vec<u8>>::new(alloc::vec![1u8; 5]), &prekey);
278        s.tag[15] ^= 0x80;
279        s.with_secret(|_| ());
280    }
281}