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
22/// Process-wide handle identifier.
23///
24/// `KeyId` is a [`NonZeroU64`] so that `Option<KeyId>` is the same size as
25/// `KeyId` itself (niche optimization), and so that `0` is unambiguously not a
26/// valid handle. Identifiers are allocated from a single process-global
27/// counter (crate-internal); they are unique within the lifetime of a process
28/// and **not** portable across runs.
29#[derive(Clone, Copy, PartialEq, Eq, Hash)]
30pub struct KeyId(NonZeroU64);
31
32impl KeyId {
33    /// Allocate the next identifier.
34    ///
35    /// Identifiers start at 1 and increase monotonically. The counter is
36    /// process-global; overflow is treated as an internal invariant
37    /// violation and the function will saturate at `u64::MAX` rather than
38    /// wrap. In practice no process will allocate 2⁶⁴ handles.
39    #[must_use]
40    pub(crate) fn next() -> Self {
41        static COUNTER: AtomicU64 = AtomicU64::new(1);
42        // Saturating add prevents wrap-around producing duplicates. With a 64-bit
43        // counter incremented once per vault key registration the saturation
44        // arm is unreachable in any realistic process.
45        let raw = COUNTER.fetch_add(1, Ordering::Relaxed);
46        let raw = if raw == 0 { 1 } else { raw };
47        // SAFETY: `raw` is always at least 1 because the counter starts at 1
48        // and we replaced any observed 0 with 1 above.
49        let id = unsafe { NonZeroU64::new_unchecked(raw) };
50        Self(id)
51    }
52
53    /// Construct a `KeyId` from a known non-zero value.
54    ///
55    /// Crate-internal: tests and the vault use this when materializing handles
56    /// from a recovered state. External code must use [`KeyId::next`] (which is
57    /// itself not part of the public API).
58    #[allow(dead_code)] // wired up by the vault in Phase 0.3.
59    #[must_use]
60    pub(crate) fn from_raw(raw: NonZeroU64) -> Self {
61        Self(raw)
62    }
63
64    /// Return the raw numeric identifier.
65    ///
66    /// Crate-internal so that the public API never exposes it. Useful inside the
67    /// vault for indexing into the internal handle table.
68    #[allow(dead_code)] // consumed by the vault registry in Phase 0.3.
69    #[must_use]
70    pub(crate) fn get(self) -> NonZeroU64 {
71        self.0
72    }
73}
74
75impl fmt::Debug for KeyId {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        // Even the id is never printed — KeyId mostly exists to make APIs
78        // type-safe inside the crate.
79        f.write_str("KeyId(<redacted>)")
80    }
81}
82
83/// Opaque, redacted reference to a key stored inside a
84/// [`KeyVault`](crate::KeyVault).
85///
86/// A `KeyHandle` is cheap to clone (it is `Copy`-shaped — currently `Clone +
87/// Copy`) and safe to pass across threads. It exposes no methods that return
88/// raw key bytes; all operations that need the underlying material are performed
89/// by the vault on the caller's behalf.
90///
91/// # Examples
92///
93/// ```
94/// use key_vault::KeyHandle;
95///
96/// // Handles are only constructed by the vault. In tests you can construct one
97/// // via the unit-tested helper. The important property is opacity:
98/// # let h = KeyHandle::__for_test();
99/// let rendered = format!("{h:?}");
100/// assert!(rendered.contains("redacted"));
101/// ```
102#[derive(Clone, Copy, PartialEq, Eq, Hash)]
103pub struct KeyHandle {
104    id: KeyId,
105}
106
107impl KeyHandle {
108    /// Allocate a fresh handle backed by a freshly-issued [`KeyId`].
109    ///
110    /// Crate-internal — only the vault is allowed to mint handles.
111    #[must_use]
112    pub(crate) fn allocate() -> Self {
113        Self { id: KeyId::next() }
114    }
115
116    /// Construct a handle from an existing identifier.
117    #[allow(dead_code)] // wired up by the vault registry in Phase 0.3.
118    #[must_use]
119    pub(crate) fn from_id(id: KeyId) -> Self {
120        Self { id }
121    }
122
123    /// Return the underlying identifier. Crate-internal.
124    #[allow(dead_code)] // consumed by the vault registry in Phase 0.3.
125    #[must_use]
126    pub(crate) fn id(self) -> KeyId {
127        self.id
128    }
129
130    /// Construct a placeholder handle for use in doctests and unit tests.
131    ///
132    /// **Not part of the supported public API.** This exists only so that
133    /// rustdoc examples can demonstrate opacity without first standing up a full
134    /// vault. The underlying id is freshly allocated from the global counter;
135    /// there is no key material associated with it.
136    #[doc(hidden)]
137    #[must_use]
138    pub fn __for_test() -> Self {
139        Self::allocate()
140    }
141}
142
143impl fmt::Debug for KeyHandle {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        // CRITICAL: never print the inner id. The whole point of the type is
146        // that nothing escapes through Debug.
147        f.write_str("KeyHandle(<redacted>)")
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use alloc::format;
155
156    #[test]
157    fn debug_is_redacted() {
158        let h = KeyHandle::allocate();
159        let rendered = format!("{h:?}");
160        assert_eq!(rendered, "KeyHandle(<redacted>)");
161    }
162
163    #[test]
164    fn debug_never_prints_inner_id() {
165        // Generate a bunch of handles and confirm none of them leaks a digit.
166        for _ in 0..1024 {
167            let h = KeyHandle::allocate();
168            let rendered = format!("{h:?}");
169            assert!(
170                !rendered.chars().any(|c| c.is_ascii_digit()),
171                "KeyHandle Debug must not leak the inner id (got {rendered:?})"
172            );
173        }
174    }
175
176    #[test]
177    fn key_id_debug_is_redacted() {
178        let id = KeyId::next();
179        let rendered = format!("{id:?}");
180        assert_eq!(rendered, "KeyId(<redacted>)");
181    }
182
183    #[test]
184    fn ids_are_unique_and_monotonic() {
185        let a = KeyId::next();
186        let b = KeyId::next();
187        let c = KeyId::next();
188        assert!(a != b);
189        assert!(b != c);
190        assert!(a.get() < b.get());
191        assert!(b.get() < c.get());
192    }
193
194    #[test]
195    fn handles_are_distinct() {
196        let h1 = KeyHandle::allocate();
197        let h2 = KeyHandle::allocate();
198        assert!(h1 != h2);
199    }
200
201    #[test]
202    fn handles_compare_by_id() {
203        let id = KeyId::next();
204        let h1 = KeyHandle::from_id(id);
205        let h2 = KeyHandle::from_id(id);
206        assert_eq!(h1, h2);
207    }
208}