Skip to main content

key_vault/
handle.rs

1//! Opaque key references.
2//!
3//! A [`KeyHandle`] is the only thing client code ever receives in exchange for
4//! registering a key. It carries no usable cryptographic material on its own —
5//! defragmentation and codex decode happen inside the vault, in scratch memory,
6//! and the result is never exposed as a raw `&[u8]` through the public API.
7//!
8//! # Opacity guarantee
9//!
10//! The [`Debug`] implementation prints `KeyHandle(<redacted>)` regardless of the
11//! underlying identifier. Handles are not [`serde::Serialize`]; they are not
12//! [`Display`]; their internal id is `pub(crate)` only. If you find yourself
13//! reaching for the raw id from outside the crate, you are bypassing a defense
14//! layer and the API should grow a method instead.
15//!
16//! [`Display`]: core::fmt::Display
17
18use core::fmt;
19use core::num::NonZeroU64;
20use core::sync::atomic::{AtomicU64, Ordering};
21
22use subtle::{Choice, ConstantTimeEq};
23
24/// Process-wide handle identifier.
25///
26/// `KeyId` is a [`NonZeroU64`] so that `Option<KeyId>` is the same size as
27/// `KeyId` itself (niche optimization), and so that `0` is unambiguously not a
28/// valid handle. Identifiers are allocated from a single process-global
29/// counter (crate-internal); they are unique within the lifetime of a process
30/// and **not** portable across runs.
31#[derive(Clone, Copy, PartialEq, Eq, Hash)]
32pub struct KeyId(NonZeroU64);
33
34impl KeyId {
35    /// Allocate the next identifier.
36    ///
37    /// Identifiers start at 1 and increase monotonically. The counter is
38    /// process-global; overflow is treated as an internal invariant
39    /// violation and the function will saturate at `u64::MAX` rather than
40    /// wrap. In practice no process will allocate 2⁶⁴ handles.
41    #[must_use]
42    pub(crate) fn next() -> Self {
43        static COUNTER: AtomicU64 = AtomicU64::new(1);
44        // Saturating add prevents wrap-around producing duplicates. With a 64-bit
45        // counter incremented once per vault key registration the saturation
46        // arm is unreachable in any realistic process.
47        let raw = COUNTER.fetch_add(1, Ordering::Relaxed);
48        let raw = if raw == 0 { 1 } else { raw };
49        // SAFETY: `raw` is always at least 1 because the counter starts at 1
50        // and we replaced any observed 0 with 1 above.
51        let id = unsafe { NonZeroU64::new_unchecked(raw) };
52        Self(id)
53    }
54
55    /// Construct a `KeyId` from a known non-zero value.
56    ///
57    /// Crate-internal: tests and the vault use this when materializing handles
58    /// from a recovered state. External code must use [`KeyId::next`] (which is
59    /// itself not part of the public API).
60    #[allow(dead_code)] // wired up by the vault in Phase 0.3.
61    #[must_use]
62    pub(crate) fn from_raw(raw: NonZeroU64) -> Self {
63        Self(raw)
64    }
65
66    /// Return the raw numeric identifier.
67    ///
68    /// Crate-internal so that the public API never exposes it. Useful inside the
69    /// vault for indexing into the internal handle table.
70    #[allow(dead_code)] // consumed by the vault registry in Phase 0.3.
71    #[must_use]
72    pub(crate) fn get(self) -> NonZeroU64 {
73        self.0
74    }
75}
76
77impl fmt::Debug for KeyId {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        // Even the id is never printed — KeyId mostly exists to make APIs
80        // type-safe inside the crate.
81        f.write_str("KeyId(<redacted>)")
82    }
83}
84
85/// Opaque, redacted reference to a key stored inside a
86/// [`KeyVault`](crate::KeyVault).
87///
88/// A `KeyHandle` is cheap to clone (it is `Copy`-shaped — currently `Clone +
89/// Copy`) and safe to pass across threads. It exposes no methods that return
90/// raw key bytes; all operations that need the underlying material are performed
91/// by the vault on the caller's behalf.
92///
93/// # Examples
94///
95/// ```
96/// use key_vault::KeyHandle;
97///
98/// // Handles are only constructed by the vault. In tests you can construct one
99/// // via the unit-tested helper. The important property is opacity:
100/// # let h = KeyHandle::__for_test();
101/// let rendered = format!("{h:?}");
102/// assert!(rendered.contains("redacted"));
103/// ```
104///
105/// # Equality
106///
107/// `KeyHandle` implements both `PartialEq` and
108/// [`subtle::ConstantTimeEq`]. The latter is the equality check the vault
109/// uses internally: it compares both inner identifiers in constant time
110/// regardless of input values, eliminating timing side-channels even
111/// though the underlying ids are not themselves secret.
112#[derive(Clone, Copy, Eq)]
113pub struct KeyHandle {
114    id: KeyId,
115}
116
117impl ConstantTimeEq for KeyHandle {
118    fn ct_eq(&self, other: &Self) -> Choice {
119        // Compare the raw NonZeroU64 values byte-equivalently in constant
120        // time. `subtle` provides ConstantTimeEq for `u64`, so we feed it
121        // the underlying numeric representation.
122        self.id.0.get().ct_eq(&other.id.0.get())
123    }
124}
125
126impl PartialEq for KeyHandle {
127    fn eq(&self, other: &Self) -> bool {
128        bool::from(self.ct_eq(other))
129    }
130}
131
132// `Hash` must be consistent with `PartialEq`: equal handles must hash equal.
133// We derive `Eq` and implement `PartialEq` through `ConstantTimeEq` (still on
134// the same inner id), so hashing the id satisfies the invariant.
135impl core::hash::Hash for KeyHandle {
136    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
137        self.id.0.get().hash(state);
138    }
139}
140
141impl KeyHandle {
142    /// Allocate a fresh handle backed by a freshly-issued [`KeyId`].
143    ///
144    /// Crate-internal — only the vault is allowed to mint handles.
145    #[must_use]
146    pub(crate) fn allocate() -> Self {
147        Self { id: KeyId::next() }
148    }
149
150    /// Construct a handle from an existing identifier.
151    #[allow(dead_code)] // wired up by the vault registry in Phase 0.3.
152    #[must_use]
153    pub(crate) fn from_id(id: KeyId) -> Self {
154        Self { id }
155    }
156
157    /// Return the underlying identifier. Crate-internal.
158    #[allow(dead_code)] // consumed by the vault registry in Phase 0.3.
159    #[must_use]
160    pub(crate) fn id(self) -> KeyId {
161        self.id
162    }
163
164    /// Construct an unbound handle for use in doctests and unit tests.
165    ///
166    /// **Not part of the supported public API.** This exists only so that
167    /// rustdoc examples can demonstrate opacity without first standing up a full
168    /// vault. The underlying id is freshly allocated from the global counter;
169    /// there is no key material associated with it.
170    #[doc(hidden)]
171    #[must_use]
172    pub fn __for_test() -> Self {
173        Self::allocate()
174    }
175}
176
177impl fmt::Debug for KeyHandle {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        // CRITICAL: never print the inner id. The whole point of the type is
180        // that nothing escapes through Debug.
181        f.write_str("KeyHandle(<redacted>)")
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use alloc::format;
189
190    #[test]
191    fn debug_is_redacted() {
192        let h = KeyHandle::allocate();
193        let rendered = format!("{h:?}");
194        assert_eq!(rendered, "KeyHandle(<redacted>)");
195    }
196
197    #[test]
198    fn debug_never_prints_inner_id() {
199        // Generate a bunch of handles and confirm none of them leaks a digit.
200        for _ in 0..1024 {
201            let h = KeyHandle::allocate();
202            let rendered = format!("{h:?}");
203            assert!(
204                !rendered.chars().any(|c| c.is_ascii_digit()),
205                "KeyHandle Debug must not leak the inner id (got {rendered:?})"
206            );
207        }
208    }
209
210    #[test]
211    fn key_id_debug_is_redacted() {
212        let id = KeyId::next();
213        let rendered = format!("{id:?}");
214        assert_eq!(rendered, "KeyId(<redacted>)");
215    }
216
217    #[test]
218    fn ids_are_unique_and_monotonic() {
219        let a = KeyId::next();
220        let b = KeyId::next();
221        let c = KeyId::next();
222        assert!(a != b);
223        assert!(b != c);
224        assert!(a.get() < b.get());
225        assert!(b.get() < c.get());
226    }
227
228    #[test]
229    fn handles_are_distinct() {
230        let h1 = KeyHandle::allocate();
231        let h2 = KeyHandle::allocate();
232        assert!(h1 != h2);
233    }
234
235    #[test]
236    fn handles_compare_by_id() {
237        let id = KeyId::next();
238        let h1 = KeyHandle::from_id(id);
239        let h2 = KeyHandle::from_id(id);
240        assert_eq!(h1, h2);
241    }
242
243    #[test]
244    fn constant_time_eq_matches_partial_eq() {
245        use core::hash::BuildHasher;
246        use std::collections::hash_map::RandomState;
247
248        use subtle::ConstantTimeEq;
249
250        let id = KeyId::next();
251        let same_a = KeyHandle::from_id(id);
252        let same_b = KeyHandle::from_id(id);
253        let different = KeyHandle::allocate();
254
255        assert!(bool::from(same_a.ct_eq(&same_b)));
256        assert!(!bool::from(same_a.ct_eq(&different)));
257
258        // Hash invariant: equal handles must hash equal (Eq + Hash).
259        let s = RandomState::new();
260        assert_eq!(s.hash_one(same_a), s.hash_one(same_b));
261    }
262}