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}