use crate::coordinate::Coordinate;
use crate::event::EventKind;
use crate::id::{EntityIdType, EventId};
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
use chacha20poly1305::{XChaCha20Poly1305, XNonce};
use std::collections::btree_map::{BTreeMap, Entry};
use std::fmt;
use zeroize::Zeroizing;
const KEY_LEN: usize = 32;
const NONCE_LEN: usize = 24;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum KeyScopeGranularity {
#[default]
PerEntity,
PerCategory,
PerTypeId,
PerEvent,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct KeyScope(Box<[u8]>);
impl fmt::Debug for KeyScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("KeyScope(0x")?;
for byte in self.0.iter() {
write!(f, "{byte:02x}")?;
}
f.write_str(")")
}
}
const SCOPE_DISC_PER_ENTITY: u8 = 0x01;
const SCOPE_DISC_PER_CATEGORY: u8 = 0x02;
const SCOPE_DISC_PER_TYPE_ID: u8 = 0x03;
const SCOPE_DISC_PER_EVENT: u8 = 0x04;
fn scope_per_entity(entity: &str) -> KeyScope {
let mut bytes = Vec::with_capacity(1 + entity.len());
bytes.push(SCOPE_DISC_PER_ENTITY);
bytes.extend_from_slice(entity.as_bytes());
KeyScope(bytes.into_boxed_slice())
}
fn scope_per_category(category: u8) -> KeyScope {
KeyScope(vec![SCOPE_DISC_PER_CATEGORY, category].into_boxed_slice())
}
fn scope_per_type_id(kind_raw: u16) -> KeyScope {
let mut bytes = Vec::with_capacity(3);
bytes.push(SCOPE_DISC_PER_TYPE_ID);
bytes.extend_from_slice(&kind_raw.to_be_bytes());
KeyScope(bytes.into_boxed_slice())
}
fn scope_per_event(event_id: u128) -> KeyScope {
let mut bytes = Vec::with_capacity(17);
bytes.push(SCOPE_DISC_PER_EVENT);
bytes.extend_from_slice(&event_id.to_be_bytes());
KeyScope(bytes.into_boxed_slice())
}
#[must_use]
pub fn scope_for(
granularity: KeyScopeGranularity,
coordinate: &Coordinate,
event_kind: EventKind,
event_id: EventId,
) -> KeyScope {
match granularity {
KeyScopeGranularity::PerEntity => scope_per_entity(coordinate.entity()),
KeyScopeGranularity::PerCategory => scope_per_category(event_kind.category()),
KeyScopeGranularity::PerTypeId => scope_per_type_id(event_kind.as_raw_u16()),
KeyScopeGranularity::PerEvent => scope_per_event(event_id.as_u128()),
}
}
#[derive(Clone, Copy, Debug)]
pub enum ShredScope<'a> {
Entity(&'a Coordinate),
Kind(EventKind),
Event(EventId),
}
impl ShredScope<'_> {
pub(crate) fn label(&self) -> &'static str {
match self {
ShredScope::Entity(_) => "Entity",
ShredScope::Kind(_) => "Kind",
ShredScope::Event(_) => "Event",
}
}
}
impl KeyScopeGranularity {
pub(crate) fn resolve_shred_scope(self, selector: &ShredScope<'_>) -> Option<KeyScope> {
match (self, selector) {
(KeyScopeGranularity::PerEntity, ShredScope::Entity(coordinate)) => {
Some(scope_per_entity(coordinate.entity()))
}
(KeyScopeGranularity::PerCategory, ShredScope::Kind(kind)) => {
Some(scope_per_category(kind.category()))
}
(KeyScopeGranularity::PerTypeId, ShredScope::Kind(kind)) => {
Some(scope_per_type_id(kind.as_raw_u16()))
}
(KeyScopeGranularity::PerEvent, ShredScope::Event(event_id)) => {
Some(scope_per_event(event_id.as_u128()))
}
_ => None,
}
}
}
impl KeyScope {
#[must_use]
pub(crate) fn as_bytes(&self) -> &[u8] {
&self.0
}
#[must_use]
pub(crate) fn from_bytes(bytes: Vec<u8>) -> Self {
KeyScope(bytes.into_boxed_slice())
}
}
#[must_use]
pub(crate) fn payload_aad(
coordinate: &Coordinate,
event_kind: EventKind,
event_id: EventId,
) -> Vec<u8> {
let entity = coordinate.entity().as_bytes();
let scope = coordinate.scope().as_bytes();
let mut aad = Vec::with_capacity(1 + 4 + entity.len() + 4 + scope.len() + 2 + 16);
aad.push(0x01);
let entity_len = u32::try_from(entity.len()).unwrap_or(u32::MAX);
let scope_len = u32::try_from(scope.len()).unwrap_or(u32::MAX);
aad.extend_from_slice(&entity_len.to_le_bytes());
aad.extend_from_slice(entity);
aad.extend_from_slice(&scope_len.to_le_bytes());
aad.extend_from_slice(scope);
aad.extend_from_slice(&event_kind.as_raw_u16().to_le_bytes());
aad.extend_from_slice(&event_id.as_u128().to_be_bytes());
aad
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyStoreError {
Rng,
KeyInit,
Seal,
Open,
}
impl fmt::Display for KeyStoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::Rng => "CSPRNG failed to produce key material",
Self::KeyInit => "AEAD key initialization rejected the key length",
Self::Seal => "authenticated encryption failed",
Self::Open => "authenticated decryption failed",
};
f.write_str(message)
}
}
impl std::error::Error for KeyStoreError {}
pub struct PayloadKey(Zeroizing<[u8; KEY_LEN]>);
impl fmt::Debug for PayloadKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PayloadKey").finish_non_exhaustive()
}
}
impl PayloadKey {
fn generate() -> Result<Self, KeyStoreError> {
let mut key: Zeroizing<[u8; KEY_LEN]> = Zeroizing::new([0u8; KEY_LEN]);
getrandom::fill(key.as_mut_slice()).map_err(|_| KeyStoreError::Rng)?;
Ok(Self(key))
}
fn cipher(&self) -> Result<XChaCha20Poly1305, KeyStoreError> {
XChaCha20Poly1305::new_from_slice(self.0.as_slice()).map_err(|_| KeyStoreError::KeyInit)
}
pub fn seal(
&self,
nonce: &[u8; NONCE_LEN],
aad: &[u8],
plaintext: &[u8],
) -> Result<Vec<u8>, KeyStoreError> {
let cipher = self.cipher()?;
let nonce = XNonce::from_slice(nonce);
cipher
.encrypt(
nonce,
Payload {
msg: plaintext,
aad,
},
)
.map_err(|_| KeyStoreError::Seal)
}
pub fn open(
&self,
nonce: &[u8; NONCE_LEN],
aad: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, KeyStoreError> {
let cipher = self.cipher()?;
let nonce = XNonce::from_slice(nonce);
cipher
.decrypt(
nonce,
Payload {
msg: ciphertext,
aad,
},
)
.map_err(|_| KeyStoreError::Open)
}
}
pub struct KeyStore {
keys: BTreeMap<KeyScope, PayloadKey>,
granularity: KeyScopeGranularity,
dirty: bool,
absent_on_load: bool,
}
impl KeyStore {
#[must_use]
pub fn new(granularity: KeyScopeGranularity) -> Self {
Self {
keys: BTreeMap::new(),
granularity,
dirty: false,
absent_on_load: false,
}
}
#[must_use]
pub(crate) fn new_absent(granularity: KeyScopeGranularity) -> Self {
Self {
keys: BTreeMap::new(),
granularity,
dirty: false,
absent_on_load: true,
}
}
#[must_use]
pub(crate) fn was_absent_on_load(&self) -> bool {
self.absent_on_load
}
#[must_use]
pub(crate) fn is_dirty(&self) -> bool {
self.dirty
}
pub(crate) fn mark_dirty(&mut self) {
self.dirty = true;
}
#[must_use]
pub fn granularity(&self) -> KeyScopeGranularity {
self.granularity
}
#[must_use]
pub fn key_count(&self) -> usize {
self.keys.len()
}
pub fn get_or_create(&mut self, scope: &KeyScope) -> Result<&PayloadKey, KeyStoreError> {
self.absent_on_load = false;
match self.keys.entry(scope.clone()) {
Entry::Occupied(entry) => Ok(entry.into_mut()),
Entry::Vacant(entry) => {
let key = PayloadKey::generate()?;
Ok(entry.insert(key))
}
}
}
#[must_use]
pub fn get(&self, scope: &KeyScope) -> Option<&PayloadKey> {
self.keys.get(scope)
}
pub fn destroy(&mut self, scope: &KeyScope) -> bool {
let removed = self.keys.remove(scope).is_some();
if removed {
self.dirty = true;
}
removed
}
}
pub mod persist;
#[cfg(test)]
mod tests;