Skip to main content

secure_gate/traits/
reveal_secret.rs

1//! Traits for controlled, polymorphic secret revelation.
2//!
3//! This module defines the core traits that enforce explicit, auditable access to
4//! secret data across all wrapper types (`Fixed<T>`, `Dynamic<T>`, aliases, etc.).
5//!
6//! The design ensures:
7//! - No implicit borrowing (`Deref`, `AsRef`, etc.)
8//! - Scoped access is preferred (minimizes lifetime of exposed references)
9//! - Direct exposure is possible but clearly marked as an escape hatch
10//! - Owned consumption is available for FFI hand-off and type migration
11//! - Metadata (`len`, `is_empty`) is always available without full exposure
12//!
13//! # Three-Tier Access Model
14//!
15//! All secret access follows an explicit hierarchy. Prefer tiers earlier in the list:
16//!
17//! | Tier | Method | When to use |
18//! |------|--------|-------------|
19//! | 1 — Scoped borrow (preferred) | `with_secret` / `with_secret_mut` | Almost all application code |
20//! | 2 — Direct reference (escape hatch) | `expose_secret` / `expose_secret_mut` | FFI, third-party APIs requiring `&T` |
21//! | 3 — Owned consumption | `into_inner` | FFI hand-off, type migration, APIs requiring `T` by value |
22//!
23//! **Audit note:** `into_inner` does not appear in an `expose_secret*` grep sweep —
24//! audit it separately. See SECURITY.md for the full list of auditable access surfaces.
25//!
26//! # Key Traits
27//!
28//! | Trait                  | Access     | Preferred Method          | Escape Hatch             | Metadata          | Feature     |
29//! |------------------------|------------|---------------------------|--------------------------|-------------------|-------------|
30//! | [`RevealSecret`]                | Read-only  | `with_secret` (scoped)    | `expose_secret`     | `len`, `is_empty` | Always |
31//! | [`crate::RevealSecretMut`]      | Mutable    | `with_secret_mut` (scoped)| `expose_secret_mut` | Inherits above    | Always |
32//!
33//! # Security Model
34//!
35//! - **Core wrappers** (`Fixed<T>`, `Dynamic<T>`) implement both traits → full access.
36//! - **Read-only wrappers** (encoding wrappers, random types) implement only `RevealSecret` → mutation prevented.
37//! - **Zero-cost** — all methods are `#[inline(always)]` where possible.
38//! - **Scoped access preferred** — `with_secret` / `with_secret_mut` limit borrow lifetime, reducing leak risk.
39//! - **Direct exposure** (`expose_secret` / `expose_secret_mut`) is provided for legitimate needs (FFI, third-party APIs), but marked as an escape hatch.
40//! - **Owned consumption** (`into_inner`) is available when the secret must be moved out of the wrapper.
41//!   Zeroization transfers to the returned `InnerSecret<T>` — the caller must let it drop normally.
42//!
43//! # Note for `RevealSecret` Implementors
44//!
45//! Adding `into_inner` to this trait means every `RevealSecret` implementor must provide
46//! an owned-extraction implementation. For wrappers intentionally limited to borrowing
47//! semantics, implement `into_inner` with `unimplemented!()` or a compile-time guard, and
48//! document the design rationale clearly.
49//!
50//! # Usage Guidelines
51//!
52//! The preferred and recommended way to access secrets is the scoped `with_secret` /
53//! `with_secret_mut` methods. `expose_secret` / `expose_secret_mut` are escape hatches
54//! for rare cases and should be audited closely. `into_inner` is reserved for the uncommon
55//! case where ownership of the inner value is required.
56//!
57//! - **Always prefer scoped methods** (`with_secret`, `with_secret_mut`) in application code.
58//! - Use direct exposure only when necessary (e.g., passing raw pointer + length to C FFI).
59//! - Audit every `expose_secret*` call — they should be rare and well-justified.
60//! - Audit every `into_inner` call — it transfers ownership out of the wrapper's protection.
61//!
62//! # Examples
63//!
64//! Scoped (recommended):
65//!
66//! ```rust
67//! use secure_gate::{Fixed, RevealSecret};
68//!
69//! let secret = Fixed::new([42u8; 4]);
70//! let sum: u32 = secret.with_secret(|bytes| bytes.iter().map(|&b| b as u32).sum());
71//! assert_eq!(sum, 42 * 4);
72//! ```
73//!
74//! Direct (escape hatch – use with caution):
75//!
76//! ```rust
77//! use secure_gate::{Fixed, RevealSecret};
78//!
79//! let secret = Fixed::new([42u8; 4]);
80//!
81//! // Example: FFI call needing raw pointer + length
82//! // unsafe {
83//! //     c_function(secret.expose_secret().as_ptr(), secret.len());
84//! // }
85//! ```
86//!
87//! Mutable scoped:
88//!
89//! ```rust
90//! use secure_gate::{Fixed, RevealSecret, RevealSecretMut};
91//!
92//! let mut secret = Fixed::new([0u8; 4]);
93//! secret.with_secret_mut(|bytes| bytes[0] = 99);
94//! assert_eq!(secret.expose_secret()[0], 99);
95//! ```
96//!
97//! Owned consumption (into_inner):
98//!
99//! ```rust
100//! use secure_gate::{Fixed, RevealSecret};
101//!
102//! let key = Fixed::new([0xABu8; 16]);
103//! // Consumes `key`; zeroization transfers to the returned InnerSecret<[u8; 16]>.
104//! let owned: secure_gate::InnerSecret<[u8; 16]> = key.into_inner();
105//! assert_eq!(*owned, [0xABu8; 16]);
106//! assert_eq!(format!("{:?}", owned), "[REDACTED]");
107//! // `owned` zeroizes its bytes when it drops.
108//! ```
109//!
110//! Polymorphic generic code:
111//!
112//! ```rust
113//! use secure_gate::RevealSecret;
114//!
115//! fn print_length<S: RevealSecret>(secret: &S) {
116//!     println!("Length: {} bytes", secret.len());
117//! }
118//! ```
119//!
120//! These traits are the foundation of secure-gate's security model: all secret access is
121//! explicit, auditable, and controlled. Prefer scoped methods in nearly all cases.
122//!
123//! # Implementation Notes
124//!
125//! Long-lived `expose_secret()` references can defeat scoping — the borrow outlives the
126//! call site and the compiler cannot enforce that the secret is not retained. This is an
127//! intentional escape hatch for FFI and legacy APIs; audit every call site.
128
129/// Owned, zeroizing secret extracted via [`RevealSecret::into_inner`].
130///
131/// `InnerSecret<T>` preserves the zeroization contract by wrapping
132/// [`zeroize::Zeroizing<T>`], while restoring a strict redaction policy for `Debug`:
133/// formatting this type always prints `[REDACTED]`, regardless of `T`.
134///
135/// Use [`into_zeroizing`](Self::into_zeroizing) only when an API explicitly requires
136/// a `Zeroizing<T>` value.
137pub struct InnerSecret<T: zeroize::Zeroize>(zeroize::Zeroizing<T>);
138
139impl<T: zeroize::Zeroize> InnerSecret<T> {
140    #[inline(always)]
141    pub(crate) fn new(inner: T) -> Self {
142        Self(zeroize::Zeroizing::new(inner))
143    }
144
145    /// Unwraps and returns the underlying [`zeroize::Zeroizing<T>`].
146    ///
147    /// This is an explicit escape hatch for interoperability with APIs that accept
148    /// `Zeroizing<T>` directly.
149    #[inline(always)]
150    pub fn into_zeroizing(self) -> zeroize::Zeroizing<T> {
151        self.0
152    }
153}
154
155impl<T: zeroize::Zeroize> core::fmt::Debug for InnerSecret<T> {
156    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
157        f.write_str("[REDACTED]")
158    }
159}
160
161impl<T: zeroize::Zeroize> core::ops::Deref for InnerSecret<T> {
162    type Target = T;
163
164    #[inline(always)]
165    fn deref(&self) -> &Self::Target {
166        &self.0
167    }
168}
169
170/// Read-only access to a wrapped secret.
171///
172/// Implemented by [`Fixed<T>`](crate::Fixed) and [`Dynamic<T>`](crate::Dynamic).
173/// Prefer the scoped [`with_secret`](Self::with_secret) method; use
174/// [`expose_secret`](Self::expose_secret) only when a long-lived reference is
175/// unavoidable. See [`RevealSecretMut`](crate::RevealSecretMut) for the mutable
176/// counterpart.
177pub trait RevealSecret {
178    /// The inner secret type being revealed.
179    ///
180    /// This can be a sized type (e.g. `[u8; N]`, `u32`) or unsized (e.g. `str`, `[u8]`).
181    type Inner: ?Sized;
182
183    /// Provides scoped (recommended) read-only access to the secret.
184    ///
185    /// The closure receives a reference that cannot escape — the borrow ends when
186    /// the closure returns, minimizing the lifetime of the exposed secret.
187    /// Prefer this over [`expose_secret`](Self::expose_secret) in all application code.
188    ///
189    /// # Examples
190    ///
191    /// ```rust
192    /// use secure_gate::{Fixed, RevealSecret};
193    ///
194    /// let secret = Fixed::new([42u8; 4]);
195    /// let sum: u32 = secret.with_secret(|bytes| bytes.iter().map(|&b| b as u32).sum());
196    /// assert_eq!(sum, 42 * 4);
197    /// ```
198    fn with_secret<F, R>(&self, f: F) -> R
199    where
200        F: FnOnce(&Self::Inner) -> R;
201
202    /// Returns a direct (auditable) read-only reference to the secret.
203    ///
204    /// Long-lived `expose_secret()` references can defeat scoping — prefer
205    /// [`with_secret`](Self::with_secret) in application code. Use this only when
206    /// a long-lived reference is unavoidable (e.g. FFI, third-party APIs).
207    ///
208    /// # Examples
209    ///
210    /// ```rust
211    /// use secure_gate::{Fixed, RevealSecret};
212    ///
213    /// let secret = Fixed::new([42u8; 4]);
214    ///
215    /// // Auditable escape hatch — FFI use case:
216    /// // unsafe { c_fn(secret.expose_secret().as_ptr(), secret.len()); }
217    /// let _ = secret.expose_secret();
218    /// ```
219    fn expose_secret(&self) -> &Self::Inner;
220
221    /// Returns the length of the secret in bytes.
222    ///
223    /// Always safe to call — does not expose secret contents.
224    fn len(&self) -> usize;
225
226    /// Returns `true` if the secret is empty.
227    ///
228    /// Always safe to call — does not expose secret contents.
229    #[inline(always)]
230    fn is_empty(&self) -> bool {
231        self.len() == 0
232    }
233
234    /// Consumes the wrapper and returns the inner value wrapped in [`InnerSecret`],
235    /// preserving automatic zeroization on drop.
236    ///
237    /// This is the safe, idiomatic path when ownership of the secret is required — for
238    /// example, to hand the value to an API that takes `T` by value, to move between
239    /// wrapper types, or at FFI boundaries where the callee takes ownership.
240    ///
241    /// The zeroization contract transfers to the caller: when the returned
242    /// `InnerSecret<Self::Inner>` drops, it calls `Self::Inner::zeroize()` automatically,
243    /// exactly as the wrapper's own `Drop` impl would have.
244    ///
245    /// # Availability
246    ///
247    /// Only callable when `Self::Inner: Sized + Default + Zeroize`. The `Default` bound
248    /// is required to construct a zero-sentinel that the wrapper's `Drop` impl runs on
249    /// after the real secret is moved out. The `Zeroize` bound is required so the
250    /// returned `InnerSecret<T>` can call `zeroize()` on drop. For types that intentionally
251    /// omit `Default` (e.g. custom key types where an all-zero value is invalid or
252    /// dangerous), `into_inner` is not callable — use `with_secret` or `expose_secret`
253    /// instead.
254    ///
255    /// The three concrete implementations in this crate all satisfy the bounds:
256    /// - `Fixed<[u8; N]>` — `[u8; N]: Default + Zeroize` ✓
257    /// - `Dynamic<String>` — `String: Default + Zeroize` ✓
258    /// - `Dynamic<Vec<T>>` — `Vec<T>: Default + Zeroize` ✓
259    ///
260    /// # Debug Behavior
261    ///
262    /// The returned [`InnerSecret<T>`] always redacts `Debug` as `[REDACTED]`, preserving
263    /// the wrapper-level redaction invariant after ownership transfer.
264    ///
265    /// # Examples
266    ///
267    /// ```rust
268    /// use secure_gate::{Fixed, RevealSecret};
269    ///
270    /// let key = Fixed::new([0xABu8; 16]);
271    /// let owned: secure_gate::InnerSecret<[u8; 16]> = key.into_inner();
272    /// // `owned` zeroizes its 16 bytes when it drops — same guarantee as Fixed<[u8; 16]>.
273    /// assert_eq!(*owned, [0xABu8; 16]);
274    /// assert_eq!(format!("{:?}", owned), "[REDACTED]");
275    /// ```
276    ///
277    /// ```rust
278    /// # #[cfg(feature = "alloc")]
279    /// # {
280    /// use secure_gate::{Dynamic, RevealSecret};
281    ///
282    /// let pw = Dynamic::<String>::new("hunter2".to_string());
283    /// let owned: secure_gate::InnerSecret<String> = pw.into_inner();
284    /// assert_eq!(*owned, "hunter2");
285    /// // `owned` zeroizes its heap buffer when it drops.
286    /// # }
287    /// ```
288    fn into_inner(self) -> InnerSecret<Self::Inner>
289    where
290        Self: Sized,
291        Self::Inner: Sized + Default + zeroize::Zeroize;
292}