use core::fmt;
use core::num::NonZeroU64;
use core::sync::atomic::{AtomicU64, Ordering};
use subtle::{Choice, ConstantTimeEq};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyId(NonZeroU64);
impl KeyId {
#[must_use]
pub(crate) fn next() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(1);
let raw = COUNTER.fetch_add(1, Ordering::Relaxed);
let raw = if raw == 0 { 1 } else { raw };
let id = unsafe { NonZeroU64::new_unchecked(raw) };
Self(id)
}
#[allow(dead_code)] #[must_use]
pub(crate) fn from_raw(raw: NonZeroU64) -> Self {
Self(raw)
}
#[allow(dead_code)] #[must_use]
pub(crate) fn get(self) -> NonZeroU64 {
self.0
}
}
impl fmt::Debug for KeyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("KeyId(<redacted>)")
}
}
#[derive(Clone, Copy, Eq)]
pub struct KeyHandle {
id: KeyId,
}
impl ConstantTimeEq for KeyHandle {
fn ct_eq(&self, other: &Self) -> Choice {
self.id.0.get().ct_eq(&other.id.0.get())
}
}
impl PartialEq for KeyHandle {
fn eq(&self, other: &Self) -> bool {
bool::from(self.ct_eq(other))
}
}
impl core::hash::Hash for KeyHandle {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.id.0.get().hash(state);
}
}
impl KeyHandle {
#[must_use]
pub(crate) fn allocate() -> Self {
Self { id: KeyId::next() }
}
#[allow(dead_code)] #[must_use]
pub(crate) fn from_id(id: KeyId) -> Self {
Self { id }
}
#[allow(dead_code)] #[must_use]
pub(crate) fn id(self) -> KeyId {
self.id
}
#[doc(hidden)]
#[must_use]
pub fn __for_test() -> Self {
Self::allocate()
}
}
impl fmt::Debug for KeyHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("KeyHandle(<redacted>)")
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
#[test]
fn debug_is_redacted() {
let h = KeyHandle::allocate();
let rendered = format!("{h:?}");
assert_eq!(rendered, "KeyHandle(<redacted>)");
}
#[test]
fn debug_never_prints_inner_id() {
for _ in 0..1024 {
let h = KeyHandle::allocate();
let rendered = format!("{h:?}");
assert!(
!rendered.chars().any(|c| c.is_ascii_digit()),
"KeyHandle Debug must not leak the inner id (got {rendered:?})"
);
}
}
#[test]
fn key_id_debug_is_redacted() {
let id = KeyId::next();
let rendered = format!("{id:?}");
assert_eq!(rendered, "KeyId(<redacted>)");
}
#[test]
fn ids_are_unique_and_monotonic() {
let a = KeyId::next();
let b = KeyId::next();
let c = KeyId::next();
assert!(a != b);
assert!(b != c);
assert!(a.get() < b.get());
assert!(b.get() < c.get());
}
#[test]
fn handles_are_distinct() {
let h1 = KeyHandle::allocate();
let h2 = KeyHandle::allocate();
assert!(h1 != h2);
}
#[test]
fn handles_compare_by_id() {
let id = KeyId::next();
let h1 = KeyHandle::from_id(id);
let h2 = KeyHandle::from_id(id);
assert_eq!(h1, h2);
}
#[test]
fn constant_time_eq_matches_partial_eq() {
use core::hash::BuildHasher;
use std::collections::hash_map::RandomState;
use subtle::ConstantTimeEq;
let id = KeyId::next();
let same_a = KeyHandle::from_id(id);
let same_b = KeyHandle::from_id(id);
let different = KeyHandle::allocate();
assert!(bool::from(same_a.ct_eq(&same_b)));
assert!(!bool::from(same_a.ct_eq(&different)));
let s = RandomState::new();
assert_eq!(s.hash_one(same_a), s.hash_one(same_b));
}
}