use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::error::{IdentityError, Result};
use crate::trust::{Revocation, TrustGrant, TrustId};
const TRUST_FILE_VERSION: u32 = 1;
#[derive(Debug, Serialize, Deserialize)]
struct TrustGrantFile {
version: u32,
grant: TrustGrant,
}
#[derive(Debug, Serialize, Deserialize)]
struct RevocationFile {
version: u32,
revocation: Revocation,
}
const GRANTED_DIR: &str = "granted";
const RECEIVED_DIR: &str = "received";
const REVOCATIONS_DIR: &str = "revocations";
pub struct TrustStore {
base_dir: PathBuf,
}
impl TrustStore {
pub fn new(base_dir: impl Into<PathBuf>) -> Result<Self> {
let base_dir = base_dir.into();
std::fs::create_dir_all(base_dir.join(GRANTED_DIR))?;
std::fs::create_dir_all(base_dir.join(RECEIVED_DIR))?;
std::fs::create_dir_all(base_dir.join(REVOCATIONS_DIR))?;
Ok(Self { base_dir })
}
pub fn save_granted(&self, grant: &TrustGrant) -> Result<()> {
self.write_grant(grant, GRANTED_DIR)
}
pub fn save_received(&self, grant: &TrustGrant) -> Result<()> {
self.write_grant(grant, RECEIVED_DIR)
}
pub fn load_grant(&self, id: &TrustId) -> Result<TrustGrant> {
let granted_path = self.grant_path(id, GRANTED_DIR);
if granted_path.exists() {
return self.read_grant(&granted_path);
}
let received_path = self.grant_path(id, RECEIVED_DIR);
if received_path.exists() {
return self.read_grant(&received_path);
}
Err(IdentityError::NotFound(format!(
"trust grant not found: {}",
id
)))
}
pub fn list_granted(&self) -> Result<Vec<TrustId>> {
self.list_ids(GRANTED_DIR)
}
pub fn list_received(&self) -> Result<Vec<TrustId>> {
self.list_ids(RECEIVED_DIR)
}
pub fn save_revocation(&self, revocation: &Revocation) -> Result<()> {
let file = RevocationFile {
version: TRUST_FILE_VERSION,
revocation: revocation.clone(),
};
let json = serde_json::to_string_pretty(&file)
.map_err(|e| IdentityError::SerializationError(e.to_string()))?;
let path = self.revocation_path(&revocation.trust_id);
std::fs::write(&path, json.as_bytes())?;
Ok(())
}
pub fn load_revocation(&self, id: &TrustId) -> Result<Revocation> {
let path = self.revocation_path(id);
if !path.exists() {
return Err(IdentityError::NotFound(format!(
"revocation not found for trust id: {}",
id
)));
}
let bytes = std::fs::read(&path)?;
let file: RevocationFile = serde_json::from_slice(&bytes).map_err(|e| {
IdentityError::InvalidFileFormat(format!(
"failed to parse revocation file {}: {e}",
path.display()
))
})?;
Ok(file.revocation)
}
pub fn list_revocations(&self) -> Result<Vec<TrustId>> {
self.list_ids(REVOCATIONS_DIR)
}
pub fn is_revoked(&self, id: &TrustId) -> bool {
self.revocation_path(id).exists()
}
fn write_grant(&self, grant: &TrustGrant, sub_dir: &str) -> Result<()> {
let file = TrustGrantFile {
version: TRUST_FILE_VERSION,
grant: grant.clone(),
};
let json = serde_json::to_string_pretty(&file)
.map_err(|e| IdentityError::SerializationError(e.to_string()))?;
let path = self.grant_path(&grant.id, sub_dir);
std::fs::write(&path, json.as_bytes())?;
Ok(())
}
fn read_grant(&self, path: &std::path::Path) -> Result<TrustGrant> {
let bytes = std::fs::read(path)?;
let file: TrustGrantFile = serde_json::from_slice(&bytes).map_err(|e| {
IdentityError::InvalidFileFormat(format!(
"failed to parse trust grant file {}: {e}",
path.display()
))
})?;
Ok(file.grant)
}
fn grant_path(&self, id: &TrustId, sub_dir: &str) -> PathBuf {
self.base_dir.join(sub_dir).join(format!("{}.json", id.0))
}
fn revocation_path(&self, id: &TrustId) -> PathBuf {
self.base_dir
.join(REVOCATIONS_DIR)
.join(format!("{}.json", id.0))
}
fn list_ids(&self, sub_dir: &str) -> Result<Vec<TrustId>> {
let dir = self.base_dir.join(sub_dir);
let mut ids = Vec::new();
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if let Some(stem) = name_str.strip_suffix(".json") {
ids.push(TrustId(stem.to_string()));
}
}
Ok(ids)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::IdentityAnchor;
use crate::trust::{Capability, Revocation, RevocationReason, TrustGrantBuilder};
fn make_grant(grantor: &IdentityAnchor, grantee: &IdentityAnchor) -> TrustGrant {
let grantee_key = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
grantee.verifying_key_bytes(),
);
TrustGrantBuilder::new(grantor.id(), grantee.id(), grantee_key)
.capability(Capability::new("read:calendar"))
.sign(grantor.signing_key())
.expect("signing grant failed")
}
fn make_revocation(anchor: &IdentityAnchor, grant: &TrustGrant) -> Revocation {
Revocation::create(
grant.id.clone(),
anchor.id(),
RevocationReason::ManualRevocation,
anchor.signing_key(),
)
}
#[test]
fn test_trust_store_creates_subdirectories() {
let dir = tempfile::tempdir().unwrap();
let _store = TrustStore::new(dir.path()).unwrap();
assert!(dir.path().join("granted").is_dir());
assert!(dir.path().join("received").is_dir());
assert!(dir.path().join("revocations").is_dir());
}
#[test]
fn test_trust_store_save_load_granted() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(Some("grantor".to_string()));
let grantee = IdentityAnchor::new(Some("grantee".to_string()));
let grant = make_grant(&grantor, &grantee);
let id = grant.id.clone();
store.save_granted(&grant).expect("save_granted failed");
let loaded = store.load_grant(&id).expect("load_grant failed");
assert_eq!(loaded.id, grant.id);
assert_eq!(loaded.grantor, grant.grantor);
assert_eq!(loaded.grantee, grant.grantee);
assert_eq!(loaded.grant_hash, grant.grant_hash);
assert_eq!(loaded.grantor_signature, grant.grantor_signature);
}
#[test]
fn test_trust_store_save_load_received() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
let id = grant.id.clone();
store.save_received(&grant).expect("save_received failed");
let loaded = store.load_grant(&id).expect("load_grant failed");
assert_eq!(loaded.id, id);
}
#[test]
fn test_trust_store_load_grant_checks_both_dirs() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
let id = grant.id.clone();
store.save_received(&grant).unwrap();
assert!(store.load_grant(&id).is_ok());
let grant2 = make_grant(&grantee, &grantor); let id2 = grant2.id.clone();
store.save_granted(&grant2).unwrap();
assert!(store.load_grant(&id2).is_ok());
}
#[test]
fn test_trust_store_save_load_50_grants() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let mut grants = Vec::with_capacity(50);
for _ in 0..50 {
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
store.save_granted(&grant).unwrap();
grants.push(grant);
}
for original in &grants {
let loaded = store.load_grant(&original.id).expect("load failed");
assert_eq!(loaded.id, original.id);
assert_eq!(loaded.grant_hash, original.grant_hash);
}
}
#[test]
fn test_trust_store_list_granted() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let mut ids = Vec::new();
for _ in 0..5 {
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
store.save_granted(&grant).unwrap();
ids.push(grant.id);
}
let listed = store.list_granted().unwrap();
assert_eq!(listed.len(), 5);
for id in &ids {
assert!(listed.contains(id));
}
}
#[test]
fn test_trust_store_list_received() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let mut ids = Vec::new();
for _ in 0..3 {
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
store.save_received(&grant).unwrap();
ids.push(grant.id);
}
let listed = store.list_received().unwrap();
assert_eq!(listed.len(), 3);
for id in &ids {
assert!(listed.contains(id));
}
}
#[test]
fn test_revocation_store() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
let trust_id = grant.id.clone();
assert!(!store.is_revoked(&trust_id));
let revocation = make_revocation(&grantor, &grant);
store
.save_revocation(&revocation)
.expect("save_revocation failed");
assert!(store.is_revoked(&trust_id));
let loaded = store
.load_revocation(&trust_id)
.expect("load_revocation failed");
assert_eq!(loaded.trust_id, trust_id);
assert_eq!(loaded.revoker, grantor.id());
assert_eq!(loaded.reason, RevocationReason::ManualRevocation);
}
#[test]
fn test_revocation_store_list_revocations() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let mut revoked_ids = Vec::new();
for _ in 0..4 {
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
let rev = make_revocation(&grantor, &grant);
store.save_revocation(&rev).unwrap();
revoked_ids.push(grant.id);
}
let listed = store.list_revocations().unwrap();
assert_eq!(listed.len(), 4);
for id in &revoked_ids {
assert!(listed.contains(id));
}
}
#[test]
fn test_trust_store_load_grant_not_found() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let missing = TrustId("atrust_doesnotexist".to_string());
let result = store.load_grant(&missing);
assert!(matches!(result, Err(IdentityError::NotFound(_))));
}
#[test]
fn test_trust_store_load_revocation_not_found() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let missing = TrustId("atrust_nope".to_string());
let result = store.load_revocation(&missing);
assert!(matches!(result, Err(IdentityError::NotFound(_))));
}
#[test]
fn test_trust_store_is_not_revoked_without_revocation() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
store.save_granted(&grant).unwrap();
assert!(!store.is_revoked(&grant.id));
}
#[test]
fn test_trust_grant_file_format() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
store.save_granted(&grant).unwrap();
let path = dir
.path()
.join("granted")
.join(format!("{}.json", grant.id.0));
let bytes = std::fs::read(&path).unwrap();
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(value["version"], TRUST_FILE_VERSION);
assert!(value["grant"].is_object());
assert_eq!(value["grant"]["id"].as_str().unwrap(), grant.id.0);
}
#[test]
fn test_revocation_file_format() {
let dir = tempfile::tempdir().unwrap();
let store = TrustStore::new(dir.path()).unwrap();
let grantor = IdentityAnchor::new(None);
let grantee = IdentityAnchor::new(None);
let grant = make_grant(&grantor, &grantee);
let revocation = make_revocation(&grantor, &grant);
store.save_revocation(&revocation).unwrap();
let path = dir
.path()
.join("revocations")
.join(format!("{}.json", grant.id.0));
let bytes = std::fs::read(&path).unwrap();
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(value["version"], TRUST_FILE_VERSION);
assert!(value["revocation"].is_object());
assert_eq!(
value["revocation"]["trust_id"].as_str().unwrap(),
grant.id.0
);
}
}