use crate::format::AuthBlob;
use crate::key::{MASTER_KEY_LEN, MasterKey};
use crate::traits::{AuthBoundKeyStore, Availability, Backend};
use crate::{Error, Result};
use chacha20poly1305::aead::{Aead, KeyInit};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
use std::sync::{Mutex, PoisonError};
use zeroize::Zeroize;
const NONCE_LEN: usize = 24;
const TAG_LEN: usize = 16;
const AAD: &[u8] = b"qualia-bioauth mock v1";
const PAYLOAD_LEN: usize = NONCE_LEN + MASTER_KEY_LEN + TAG_LEN;
pub struct MockKeyStore {
kek: Mutex<Option<[u8; 32]>>,
}
impl MockKeyStore {
#[must_use]
pub fn new() -> Self {
Self {
kek: Mutex::new(None),
}
}
fn with_key<T>(&self, operation: impl FnOnce(&[u8; 32]) -> Result<T>) -> Result<T> {
let mut guard = self.kek.lock().unwrap_or_else(PoisonError::into_inner);
if guard.is_none() {
let mut key = [0_u8; 32];
getrandom::fill(&mut key).map_err(|_| Error::RandomnessUnavailable)?;
*guard = Some(key);
}
let key = guard.as_ref().expect("the key was just generated");
operation(key)
}
fn has_key(&self) -> bool {
self.kek
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_some()
}
}
impl Default for MockKeyStore {
fn default() -> Self {
Self::new()
}
}
impl core::fmt::Debug for MockKeyStore {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("MockKeyStore")
.field("has_key", &self.has_key())
.finish_non_exhaustive()
}
}
impl AuthBoundKeyStore for MockKeyStore {
fn availability(&self) -> Availability {
Availability::Available {
backend: Backend::Mock,
hardware_backed: false,
}
}
fn create(&self) -> Result<()> {
self.with_key(|_| Ok(()))
}
fn wrap(&self, master: &MasterKey) -> Result<Vec<u8>> {
let mut nonce = [0_u8; NONCE_LEN];
let ciphertext = self.with_key(|key| {
getrandom::fill(&mut nonce).map_err(|_| Error::RandomnessUnavailable)?;
let cipher = XChaCha20Poly1305::new(&Key::from(*key));
cipher
.encrypt(
&XNonce::from(nonce),
chacha20poly1305::aead::Payload {
msg: master.expose_secret(),
aad: AAD,
},
)
.map_err(|_| Error::Platform {
operation: "mock encrypt",
code: 0,
})
})?;
let mut payload = Vec::with_capacity(PAYLOAD_LEN);
payload.extend_from_slice(&nonce);
payload.extend_from_slice(&ciphertext);
AuthBlob::new(Backend::Mock, payload).map(|blob| blob.encode())
}
fn unwrap(&self, blob: &[u8]) -> Result<MasterKey> {
let blob = AuthBlob::parse(blob)?;
blob.expect_backend(Backend::Mock)?;
let payload = blob.wrapped();
if payload.len() != PAYLOAD_LEN {
return Err(Error::WrappedPayloadInvalid {
reason: "the mock payload does not have the fixed length",
});
}
let mut nonce = [0_u8; NONCE_LEN];
nonce.copy_from_slice(&payload[..NONCE_LEN]);
let plaintext = self.with_key(|key| {
let cipher = XChaCha20Poly1305::new(&Key::from(*key));
cipher
.decrypt(
&XNonce::from(nonce),
chacha20poly1305::aead::Payload {
msg: &payload[NONCE_LEN..],
aad: AAD,
},
)
.map_err(|_| Error::MockAuthenticationFailed)
})?;
if plaintext.len() != MASTER_KEY_LEN {
return Err(Error::WrappedPayloadInvalid {
reason: "the mock payload does not decrypt to a 32-byte key",
});
}
let mut key = [0_u8; MASTER_KEY_LEN];
key.copy_from_slice(&plaintext);
let value = MasterKey::from_array(key);
key.zeroize();
Ok(value)
}
fn delete_blob(&self, blob: &[u8]) -> Result<()> {
let blob = AuthBlob::parse(blob)?;
blob.expect_backend(Backend::Mock)?;
self.delete()
}
fn delete(&self) -> Result<()> {
let mut guard = self.kek.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(mut key) = guard.take() {
key.zeroize();
}
Ok(())
}
}
impl Drop for MockKeyStore {
fn drop(&mut self) {
if let Some(mut key) = self
.kek
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
{
key.zeroize();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_unwrap_roundtrip() {
let store = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let blob = store.wrap(&master).expect("wrap");
assert_eq!(blob.len(), crate::format::HEADER_LEN + PAYLOAD_LEN);
let restored = store.unwrap(&blob).expect("unwrap");
assert_eq!(restored, master);
}
#[test]
fn two_wraps_differ() {
let store = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let a = store.wrap(&master).expect("wrap");
let b = store.wrap(&master).expect("wrap");
assert_ne!(a, b);
assert_eq!(store.unwrap(&a).expect("a"), master);
assert_eq!(store.unwrap(&b).expect("b"), master);
}
#[test]
fn another_store_cannot_unwrap() {
let first = MockKeyStore::new();
let second = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let blob = first.wrap(&master).expect("wrap");
assert!(matches!(
second.unwrap(&blob),
Err(Error::MockAuthenticationFailed)
));
}
#[test]
fn payload_tampering_is_detected() {
let store = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let mut blob = store.wrap(&master).expect("wrap");
for index in crate::format::HEADER_LEN..blob.len() {
let original = blob[index];
blob[index] ^= 0x01;
assert!(
store.unwrap(&blob).is_err(),
"flipping byte {index} was not detected"
);
blob[index] = original;
}
}
const BACKEND_OFFSET: usize = 9;
#[test]
fn wrong_backend_is_rejected() {
let store = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let mut blob = store.wrap(&master).expect("wrap");
blob[BACKEND_OFFSET] = 2;
assert!(matches!(
store.unwrap(&blob),
Err(Error::WrongBackend {
expected: Backend::Mock,
actual: Backend::WindowsWebAuthn,
})
));
}
#[test]
fn delete_wipes_the_key() {
let store = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let blob = store.wrap(&master).expect("wrap");
store.delete().expect("delete");
assert!(!store.has_key());
assert!(store.unwrap(&blob).is_err());
let fresh = MasterKey::generate().expect("randomness");
let new_blob = store.wrap(&fresh).expect("wrap after delete");
assert_eq!(store.unwrap(&new_blob).expect("unwrap"), fresh);
}
#[test]
fn delete_blob_removes_the_key() {
let store = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let blob = store.wrap(&master).expect("wrap");
store.delete_blob(&blob).expect("delete by blob");
assert!(!store.has_key());
assert!(store.unwrap(&blob).is_err());
let mut wrong_backend = blob;
wrong_backend[BACKEND_OFFSET] = 2;
let store = MockKeyStore::new();
assert!(matches!(
store.delete_blob(&wrong_backend),
Err(Error::WrongBackend { .. })
));
}
#[test]
fn create_is_idempotent() {
let store = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let blob = store.wrap(&master).expect("wrap");
store.create().expect("create");
store.create().expect("create again");
assert_eq!(store.unwrap(&blob).expect("unwrap"), master);
}
#[test]
fn availability_is_honest() {
let store = MockKeyStore::new();
assert_eq!(
store.availability(),
Availability::Available {
backend: Backend::Mock,
hardware_backed: false,
}
);
}
#[test]
fn debug_does_not_print_the_key() {
let store = MockKeyStore::new();
store.create().expect("create");
let rendered = format!("{store:?}");
assert!(rendered.contains("has_key"));
assert!(!rendered.contains("ab"));
}
}