kernel/secret/sealed.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Sealed values: plaintext that cannot reach any sink.
7//! The part `crates/kernel/spec/Secret.lean` specifies this module with the
8//! rest of `kernel::secret`.
9
10use secrecy::{ExposeSecret, SecretBox};
11use zeroize::{Zeroize, Zeroizing};
12
13/// A value that cannot reach any sink: no Debug, no Display, no serde,
14/// no Clone; drop zeroizes (secrecy::SecretBox). Expose call sites are
15/// whitelisted by `xtask secret` (`EXPOSE_WHITELIST`).
16pub struct Sealed<T: Zeroize>(SecretBox<T>);
17
18impl<T: Zeroize> Sealed<T> {
19 pub fn new(value: Box<T>) -> Sealed<T> {
20 Sealed(SecretBox::new(value))
21 }
22
23 /// The redemption borrow. Whitelist enforced by `xtask secret`; the
24 /// type-level guarantee is that even a leaked borrow cannot be
25 /// formatted or serialized through `Sealed` itself.
26 pub fn expose(&self) -> &T {
27 self.0.expose_secret()
28 }
29}
30
31impl Sealed<String> {
32 /// The one exit besides redemption: into the vault that will hold
33 /// it. Consuming, so a caller cannot keep what it just handed over,
34 /// and the result is still a type that zeroizes on drop.
35 ///
36 /// It lives here rather than at the enrolment site because that is
37 /// what keeps the enrolment site off the expose whitelist: plaintext
38 /// leaves this type in the module that defines the type.
39 #[must_use]
40 pub fn into_vault_value(self) -> Zeroizing<String> {
41 Zeroizing::new(self.expose().clone())
42 }
43}
44
45#[cfg(test)]
46#[allow(
47 clippy::unwrap_used,
48 clippy::expect_used,
49 clippy::panic,
50 clippy::indexing_slicing,
51 reason = "test code"
52)]
53mod tests {
54 use super::*;
55 /// What the vault receives is the secret that was sealed, whole.
56 #[test]
57 fn the_vault_receives_the_sealed_secret() {
58 let sealed = Sealed::new(Box::new("hunter2".to_owned()));
59 assert_eq!(sealed.into_vault_value().as_str(), "hunter2");
60 }
61
62 #[test]
63 fn sealed_exposes_by_borrow_and_nothing_else() {
64 let sealed = Sealed::new(Box::new("hunter2".to_owned()));
65 assert_eq!(sealed.expose(), "hunter2");
66 // No Debug/Display/Serialize/Clone impls exist — pinned by the
67 // trybuild case (formatting a Sealed does not compile).
68 }
69}