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}