use super::backend::{FileKeysetBackendRef, KeysetBackend};
use super::{KeyScope, KeyScopeGranularity, KeyStore, PayloadKey, KEY_LEN};
use crate::store::platform::fs::{RealFs, StoreFs};
use crate::store::StoreError;
use std::collections::BTreeMap;
use std::path::Path;
use zeroize::{Zeroize, Zeroizing};
pub(crate) const KEYSET_MAGIC: &[u8; 6] = b"FBATKS";
pub(crate) const KEYSET_VERSION: u16 = 1;
const DISC_PER_ENTITY: u8 = super::SCOPE_DISC_PER_ENTITY;
const DISC_PER_CATEGORY: u8 = super::SCOPE_DISC_PER_CATEGORY;
const DISC_PER_TYPE_ID: u8 = super::SCOPE_DISC_PER_TYPE_ID;
const DISC_PER_EVENT: u8 = super::SCOPE_DISC_PER_EVENT;
fn granularity_to_disc(granularity: KeyScopeGranularity) -> u8 {
match granularity {
KeyScopeGranularity::PerEntity => DISC_PER_ENTITY,
KeyScopeGranularity::PerCategory => DISC_PER_CATEGORY,
KeyScopeGranularity::PerTypeId => DISC_PER_TYPE_ID,
KeyScopeGranularity::PerEvent => DISC_PER_EVENT,
}
}
fn granularity_from_disc(disc: u8) -> Option<KeyScopeGranularity> {
match disc {
DISC_PER_ENTITY => Some(KeyScopeGranularity::PerEntity),
DISC_PER_CATEGORY => Some(KeyScopeGranularity::PerCategory),
DISC_PER_TYPE_ID => Some(KeyScopeGranularity::PerTypeId),
DISC_PER_EVENT => Some(KeyScopeGranularity::PerEvent),
_ => None,
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct KeysetWire {
granularity: u8,
entries: Vec<KeysetEntryWire>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct KeysetEntryWire {
scope: Vec<u8>,
key: [u8; KEY_LEN],
}
fn corrupt(reason: String) -> StoreError {
StoreError::KeysetCorrupt { reason }
}
impl KeyStore {
pub fn flush(&mut self, dir: &Path) -> Result<(), StoreError> {
self.flush_with_fs(dir, &RealFs)
}
pub(crate) fn flush_with_fs(&mut self, dir: &Path, fs: &dyn StoreFs) -> Result<(), StoreError> {
self.flush_with_backend(&FileKeysetBackendRef { dir, fs })
}
pub(crate) fn flush_with_backend(
&mut self,
backend: &dyn KeysetBackend,
) -> Result<(), StoreError> {
let image = self.encoded_image()?;
backend.persist(&image)?;
self.dirty = false;
tracing::debug!(
target: "batpak::keyscope",
count = self.keys.len(),
"flushed crypto-shred keyset"
);
Ok(())
}
fn encoded_image(&self) -> Result<Zeroizing<Vec<u8>>, StoreError> {
let mut wire = KeysetWire {
granularity: granularity_to_disc(self.granularity),
entries: Vec::with_capacity(self.keys.len()),
};
for (scope, key) in &self.keys {
wire.entries.push(KeysetEntryWire {
scope: scope.0.to_vec(),
key: *key.0,
});
}
let body = Zeroizing::new(
crate::encoding::to_bytes(&wire)
.map_err(|error| StoreError::ser_msg(&format!("encode keyset: {error}")))?,
);
for entry in &mut wire.entries {
entry.key.zeroize();
}
let crc = crc32fast::hash(&body);
let mut image = Zeroizing::new(Vec::with_capacity(
crate::store::wire_header::HEADER_LEN + body.len(),
));
image.extend_from_slice(&crate::store::wire_header::encode(
KEYSET_MAGIC,
KEYSET_VERSION,
crc,
));
image.extend_from_slice(&body);
Ok(image)
}
pub fn load(dir: &Path, granularity: KeyScopeGranularity) -> Result<Self, StoreError> {
Self::load_with_fs(dir, &RealFs, granularity)
}
pub(crate) fn load_with_fs(
dir: &Path,
fs: &dyn StoreFs,
granularity: KeyScopeGranularity,
) -> Result<Self, StoreError> {
Self::load_with_backend(&FileKeysetBackendRef { dir, fs }, granularity)
}
pub(crate) fn load_with_backend(
backend: &dyn KeysetBackend,
granularity: KeyScopeGranularity,
) -> Result<Self, StoreError> {
match backend.load()? {
Some(raw) => decode_keyset(&raw, granularity),
None => Ok(Self::new_absent(granularity)),
}
}
}
fn validate_header_and_body(raw: &[u8]) -> Result<&[u8], StoreError> {
let prefix = match crate::store::wire_header::parse(raw, KEYSET_MAGIC) {
Ok(prefix) => prefix,
Err(crate::store::wire_header::PrefixError::TooShort { len }) => {
return Err(corrupt(format!("file too short: {len} bytes")));
}
Err(crate::store::wire_header::PrefixError::BadMagic) => {
return Err(corrupt("wrong magic bytes".to_owned()));
}
};
let version = prefix.version;
if version != KEYSET_VERSION {
return Err(corrupt(format!(
"unsupported keyset version {version}; this binary reads and writes version \
{KEYSET_VERSION}"
)));
}
let stored_crc = prefix.stored_crc;
let body = prefix.body;
let computed_crc = crc32fast::hash(body);
if stored_crc != computed_crc {
return Err(corrupt(format!(
"crc mismatch: stored {stored_crc:#010x}, computed {computed_crc:#010x}"
)));
}
Ok(body)
}
fn decode_keyset(raw: &[u8], configured: KeyScopeGranularity) -> Result<KeyStore, StoreError> {
let body = validate_header_and_body(raw)?;
let mut wire: KeysetWire = crate::encoding::from_bytes(body)
.map_err(|error| corrupt(format!("decode keyset body: {error}")))?;
let result = rehydrate(&wire, configured);
for entry in &mut wire.entries {
entry.key.zeroize();
}
result
}
fn rehydrate(wire: &KeysetWire, configured: KeyScopeGranularity) -> Result<KeyStore, StoreError> {
let persisted = granularity_from_disc(wire.granularity).ok_or_else(|| {
corrupt(format!(
"unknown key-scope granularity discriminant {}",
wire.granularity
))
})?;
if persisted != configured {
return Err(corrupt(format!(
"configured key-scope granularity {configured:?} does not match persisted keyset \
granularity {persisted:?}"
)));
}
let mut keys = BTreeMap::new();
for entry in &wire.entries {
let scope = KeyScope(entry.scope.clone().into_boxed_slice());
let key = PayloadKey(Zeroizing::new(entry.key));
keys.insert(scope, key);
}
Ok(KeyStore {
keys,
granularity: configured,
dirty: false,
absent_on_load: false,
})
}
#[cfg(test)]
mod tests;
#[cfg(all(test, feature = "dangerous-test-hooks"))]
mod crash_tests;