Skip to main content

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}