extern crate std;
use std::collections::BTreeMap;
use std::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum AuthAction {
Login = 0,
Logout = 1,
Challenge = 2,
TokenRefresh = 3,
KeyRotation = 4,
Revocation = 5,
}
#[derive(Debug, Clone)]
pub struct AuthAuditLog {
pub identity_hash: [u8; 32],
pub timestamp_ms: u64,
pub action: AuthAction,
pub success: bool,
pub zkp_verified: bool,
}
impl AuthAuditLog {
pub fn to_bytes(&self) -> [u8; 43] {
let mut buf = [0u8; 43];
buf[0..32].copy_from_slice(&self.identity_hash);
buf[32..40].copy_from_slice(&self.timestamp_ms.to_le_bytes());
buf[40] = self.action as u8;
buf[41] = self.success as u8;
buf[42] = self.zkp_verified as u8;
buf
}
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
if buf.len() < 43 {
return None;
}
let mut identity_hash = [0u8; 32];
identity_hash.copy_from_slice(&buf[0..32]);
Some(Self {
identity_hash,
timestamp_ms: u64::from_le_bytes(buf[32..40].try_into().ok()?),
action: match buf[40] {
0 => AuthAction::Login,
1 => AuthAction::Logout,
2 => AuthAction::Challenge,
3 => AuthAction::TokenRefresh,
4 => AuthAction::KeyRotation,
5 => AuthAction::Revocation,
_ => return None,
},
success: buf[41] != 0,
zkp_verified: buf[42] != 0,
})
}
}
pub struct AuthDbStore {
store: BTreeMap<[u8; 40], [u8; 43]>,
pub total_entries: u64,
}
impl Default for AuthDbStore {
fn default() -> Self {
Self::new()
}
}
impl AuthDbStore {
pub const fn new() -> Self {
Self {
store: BTreeMap::new(),
total_entries: 0,
}
}
#[inline]
fn make_key(id_hash: &[u8; 32], timestamp_ms: u64) -> [u8; 40] {
let mut key = [0u8; 40];
key[0..32].copy_from_slice(id_hash);
key[32..40].copy_from_slice(×tamp_ms.to_be_bytes());
key
}
pub fn store_audit(&mut self, log: &AuthAuditLog) {
let key = Self::make_key(&log.identity_hash, log.timestamp_ms);
self.store.insert(key, log.to_bytes());
self.total_entries = self.store.len() as u64;
}
pub fn query_by_identity(&self, id_hash: &[u8; 32], from_ms: u64) -> Vec<AuthAuditLog> {
let start = Self::make_key(id_hash, from_ms);
let end = Self::make_key(id_hash, u64::MAX);
self.store
.range(start..=end)
.filter_map(|(_k, v)| AuthAuditLog::from_bytes(v))
.collect()
}
pub fn count_failed_attempts(&self, id_hash: &[u8; 32], from_ms: u64) -> u64 {
self.query_by_identity(id_hash, from_ms)
.iter()
.filter(|log| !log.success)
.count() as u64
}
pub fn len(&self) -> usize {
self.store.len()
}
pub fn is_empty(&self) -> bool {
self.store.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_log_serialization() {
let log = AuthAuditLog {
identity_hash: [0xAB; 32],
timestamp_ms: 1234567890,
action: AuthAction::Login,
success: true,
zkp_verified: true,
};
let bytes = log.to_bytes();
let restored = AuthAuditLog::from_bytes(&bytes).unwrap();
assert_eq!(restored.identity_hash, [0xAB; 32]);
assert_eq!(restored.action, AuthAction::Login);
assert!(restored.success);
assert!(restored.zkp_verified);
}
#[test]
fn test_action_values() {
assert_eq!(AuthAction::Login as u8, 0);
assert_eq!(AuthAction::KeyRotation as u8, 4);
assert_eq!(AuthAction::Revocation as u8, 5);
}
#[test]
fn test_all_action_roundtrip() {
for (byte, expected) in [
(0, AuthAction::Login),
(1, AuthAction::Logout),
(2, AuthAction::Challenge),
(3, AuthAction::TokenRefresh),
(4, AuthAction::KeyRotation),
(5, AuthAction::Revocation),
] {
let log = AuthAuditLog {
identity_hash: [0; 32],
timestamp_ms: 100,
action: expected,
success: false,
zkp_verified: false,
};
let bytes = log.to_bytes();
assert_eq!(bytes[40], byte);
let restored = AuthAuditLog::from_bytes(&bytes).unwrap();
assert_eq!(restored.action, expected);
}
}
#[test]
fn test_from_bytes_too_short() {
assert!(AuthAuditLog::from_bytes(&[0u8; 42]).is_none());
assert!(AuthAuditLog::from_bytes(&[]).is_none());
}
#[test]
fn test_from_bytes_invalid_action() {
let mut buf = [0u8; 43];
buf[40] = 99;
assert!(AuthAuditLog::from_bytes(&buf).is_none());
}
#[test]
fn test_success_and_zkp_flags() {
let log = AuthAuditLog {
identity_hash: [0; 32],
timestamp_ms: 0,
action: AuthAction::Login,
success: false,
zkp_verified: false,
};
let bytes = log.to_bytes();
assert_eq!(bytes[41], 0);
assert_eq!(bytes[42], 0);
let log2 = AuthAuditLog {
success: true,
zkp_verified: true,
..log
};
let bytes2 = log2.to_bytes();
assert_eq!(bytes2[41], 1);
assert_eq!(bytes2[42], 1);
}
#[test]
fn test_to_bytes_length() {
let log = AuthAuditLog {
identity_hash: [0xFF; 32],
timestamp_ms: u64::MAX,
action: AuthAction::Challenge,
success: true,
zkp_verified: false,
};
assert_eq!(log.to_bytes().len(), 43);
}
#[test]
fn test_timestamp_roundtrip() {
let ts = 0xDEAD_BEEF_CAFE_1234u64;
let log = AuthAuditLog {
identity_hash: [0; 32],
timestamp_ms: ts,
action: AuthAction::Login,
success: true,
zkp_verified: true,
};
let restored = AuthAuditLog::from_bytes(&log.to_bytes()).unwrap();
assert_eq!(restored.timestamp_ms, ts);
}
#[test]
fn test_store_and_query() {
let mut store = AuthDbStore::new();
let id = [0xAA; 32];
store.store_audit(&AuthAuditLog {
identity_hash: id,
timestamp_ms: 1000,
action: AuthAction::Login,
success: true,
zkp_verified: true,
});
store.store_audit(&AuthAuditLog {
identity_hash: id,
timestamp_ms: 2000,
action: AuthAction::Logout,
success: true,
zkp_verified: false,
});
let logs = store.query_by_identity(&id, 0);
assert_eq!(logs.len(), 2);
assert_eq!(logs[0].action, AuthAction::Login);
assert_eq!(logs[1].action, AuthAction::Logout);
}
#[test]
fn test_store_query_time_range() {
let mut store = AuthDbStore::new();
let id = [0xBB; 32];
for ts in [100, 200, 300, 400, 500] {
store.store_audit(&AuthAuditLog {
identity_hash: id,
timestamp_ms: ts,
action: AuthAction::Challenge,
success: true,
zkp_verified: false,
});
}
let logs = store.query_by_identity(&id, 300);
assert_eq!(logs.len(), 3);
assert_eq!(logs[0].timestamp_ms, 300);
}
#[test]
fn test_count_failed_attempts() {
let mut store = AuthDbStore::new();
let id = [0xCC; 32];
store.store_audit(&AuthAuditLog {
identity_hash: id,
timestamp_ms: 100,
action: AuthAction::Login,
success: false,
zkp_verified: false,
});
store.store_audit(&AuthAuditLog {
identity_hash: id,
timestamp_ms: 200,
action: AuthAction::Login,
success: true,
zkp_verified: true,
});
store.store_audit(&AuthAuditLog {
identity_hash: id,
timestamp_ms: 300,
action: AuthAction::Login,
success: false,
zkp_verified: false,
});
assert_eq!(store.count_failed_attempts(&id, 0), 2);
assert_eq!(store.count_failed_attempts(&id, 200), 1);
}
#[test]
fn test_store_separate_identities() {
let mut store = AuthDbStore::new();
let id_a = [0x11; 32];
let id_b = [0x22; 32];
store.store_audit(&AuthAuditLog {
identity_hash: id_a,
timestamp_ms: 100,
action: AuthAction::Login,
success: true,
zkp_verified: true,
});
store.store_audit(&AuthAuditLog {
identity_hash: id_b,
timestamp_ms: 200,
action: AuthAction::Login,
success: true,
zkp_verified: true,
});
assert_eq!(store.query_by_identity(&id_a, 0).len(), 1);
assert_eq!(store.query_by_identity(&id_b, 0).len(), 1);
assert_eq!(store.total_entries, 2);
}
#[test]
fn test_store_len_and_empty() {
let mut store = AuthDbStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
store.store_audit(&AuthAuditLog {
identity_hash: [0; 32],
timestamp_ms: 0,
action: AuthAction::Login,
success: true,
zkp_verified: false,
});
assert!(!store.is_empty());
assert_eq!(store.len(), 1);
}
}