Skip to main content

orbit_auth/
keyring.rs

1use std::collections::BTreeMap;
2use std::fmt;
3
4use hkdf::Hkdf;
5use sha2::Sha256;
6use zeroize::Zeroizing;
7
8use crate::{Error, Result};
9
10pub type KeyId = u32;
11
12/// High-entropy 256-bit key material, zeroized on drop. Never use a password here.
13pub struct SecretKey(pub(crate) Zeroizing<[u8; 32]>);
14
15impl SecretKey {
16    pub fn generate() -> Result<Self> {
17        let mut bytes = Zeroizing::new([0; 32]);
18        getrandom::fill(bytes.as_mut()).map_err(|_| Error::Randomness)?;
19        Ok(Self(bytes))
20    }
21
22    /// Import key material from the application's secret manager.
23    /// The caller owns any copies it makes before passing this array.
24    pub fn from_bytes(bytes: [u8; 32]) -> Self {
25        Self(Zeroizing::new(bytes))
26    }
27}
28
29impl fmt::Debug for SecretKey {
30    fn fmt(
31        &self,
32        f: &mut fmt::Formatter<'_>,
33    ) -> fmt::Result {
34        f.write_str("SecretKey([REDACTED])")
35    }
36}
37
38/// Immutable rotation snapshot: issue with the active key, validate with any retained key.
39/// Replace the authority to rotate; removing or replacing a key invalidates cache namespaces.
40pub struct Keyring {
41    pub(crate) active: KeyId,
42    pub(crate) keys: BTreeMap<KeyId, SecretKey>,
43}
44
45impl Keyring {
46    pub fn new(
47        active: KeyId,
48        keys: impl IntoIterator<Item = (KeyId, SecretKey)>,
49    ) -> Result<Self> {
50        let mut retained = BTreeMap::new();
51        for (id, key) in keys {
52            if retained.insert(id, key).is_some() {
53                return Err(Error::DuplicateKey);
54            }
55        }
56        if !retained.contains_key(&active) {
57            return Err(Error::MissingActiveKey);
58        }
59        Ok(Self { active, keys: retained })
60    }
61}
62
63pub(crate) fn derive(
64    key: &SecretKey,
65    context: &[u8],
66) -> Zeroizing<[u8; 32]> {
67    let mut result = Zeroizing::new([0; 32]);
68    Hkdf::<Sha256>::new(Some(b"orbit-auth/v1/hkdf-sha256"), key.0.as_ref())
69        .expand(context, result.as_mut())
70        .expect("32 bytes is a valid HKDF-SHA256 output length");
71    result
72}
73
74/// Length prefixes make compound context labels unambiguous.
75pub(crate) fn context(parts: &[&[u8]]) -> Vec<u8> {
76    let mut bytes = Vec::new();
77    for part in parts {
78        bytes.extend_from_slice(&(part.len() as u64).to_be_bytes());
79        bytes.extend_from_slice(part);
80    }
81    bytes
82}