secure_gate/sealed/
dynamic.rs1use 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
15pub trait SealedPayload: zeroize::Zeroize + private::Sealed + Sized {
18 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 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 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#[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 pub fn seal(secret: Dynamic<T>, prekey: &Arc<Prekey>) -> Self {
108 let salt = Prekey::fresh_salt();
109 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 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 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 pub fn unseal(self) -> Dynamic<T> {
162 scrubbed(|| T::from_plain(self.open()))
163 }
164
165 #[inline]
168 pub fn len(&self) -> usize {
169 self.ct.len()
170 }
171
172 #[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#[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
214impl<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 #[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}