use crate::did::{GeneratedKey, KeyType};
use crate::error::{Error, Result};
use crate::key_manager::{Secret, SecretMaterial, SecretType};
use base64::Engine;
use dirs::home_dir;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
pub const DEFAULT_TAP_DIR: &str = ".tap";
pub const DEFAULT_KEYS_FILE: &str = "keys.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredKey {
pub did: String,
#[serde(default)]
pub label: String,
#[serde(with = "key_type_serde")]
pub key_type: KeyType,
pub private_key: String,
pub public_key: String,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
mod key_type_serde {
use super::KeyType;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(key_type: &KeyType, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = match key_type {
#[cfg(feature = "crypto-ed25519")]
KeyType::Ed25519 => "Ed25519",
#[cfg(feature = "crypto-p256")]
KeyType::P256 => "P256",
#[cfg(feature = "crypto-secp256k1")]
KeyType::Secp256k1 => "Secp256k1",
};
serializer.serialize_str(s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<KeyType, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
#[cfg(feature = "crypto-ed25519")]
"Ed25519" => Ok(KeyType::Ed25519),
#[cfg(feature = "crypto-p256")]
"P256" => Ok(KeyType::P256),
#[cfg(feature = "crypto-secp256k1")]
"Secp256k1" => Ok(KeyType::Secp256k1),
_ => Err(serde::de::Error::custom(format!(
"Unknown or disabled key type: {}",
s
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::env;
use tempfile::TempDir;
#[test]
#[serial]
fn test_tap_home_environment_variable() {
let old_home = env::var("TAP_HOME").ok();
let old_test = env::var("TAP_TEST_DIR").ok();
env::remove_var("TAP_HOME");
env::remove_var("TAP_TEST_DIR");
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_path_buf();
env::set_var("TAP_HOME", &temp_path);
let key_path = KeyStorage::default_key_path().unwrap();
assert_eq!(key_path, temp_path.join(DEFAULT_KEYS_FILE));
env::remove_var("TAP_HOME");
if let Some(val) = old_home {
env::set_var("TAP_HOME", val);
}
if let Some(val) = old_test {
env::set_var("TAP_TEST_DIR", val);
}
}
#[test]
#[serial]
fn test_tap_test_dir_environment_variable() {
let old_home = env::var("TAP_HOME").ok();
let old_test = env::var("TAP_TEST_DIR").ok();
env::remove_var("TAP_HOME");
env::remove_var("TAP_TEST_DIR");
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_path_buf();
env::set_var("TAP_TEST_DIR", &temp_path);
let key_path = KeyStorage::default_key_path().unwrap();
let expected_path = temp_path.join(DEFAULT_TAP_DIR).join(DEFAULT_KEYS_FILE);
assert_eq!(key_path, expected_path);
env::remove_var("TAP_TEST_DIR");
if let Some(val) = old_home {
env::set_var("TAP_HOME", val);
}
if let Some(val) = old_test {
env::set_var("TAP_TEST_DIR", val);
}
drop(temp_dir);
}
#[test]
#[serial]
fn test_environment_variable_priority() {
let old_home = env::var("TAP_HOME").ok();
let old_test = env::var("TAP_TEST_DIR").ok();
let home_dir = TempDir::new().unwrap();
let test_dir = TempDir::new().unwrap();
let home_path = home_dir.path().to_path_buf();
let test_path = test_dir.path().to_path_buf();
env::set_var("TAP_HOME", &home_path);
env::set_var("TAP_TEST_DIR", &test_path);
let key_path = KeyStorage::default_key_path().unwrap();
assert_eq!(key_path, home_path.join(DEFAULT_KEYS_FILE));
env::remove_var("TAP_HOME");
env::remove_var("TAP_TEST_DIR");
if let Some(val) = old_home {
env::set_var("TAP_HOME", val);
}
if let Some(val) = old_test {
env::set_var("TAP_TEST_DIR", val);
}
}
#[test]
#[serial]
fn test_agent_directory_with_tap_home() {
let old_home = env::var("TAP_HOME").ok();
let old_test = env::var("TAP_TEST_DIR").ok();
env::remove_var("TAP_HOME");
env::remove_var("TAP_TEST_DIR");
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_path_buf();
env::set_var("TAP_HOME", &temp_path);
let storage = KeyStorage::new();
let sanitized = sanitize_did("did:key:test123");
let agent_dir = storage.get_agent_directory(&sanitized).unwrap();
assert_eq!(agent_dir, temp_path.join("did_key_test123"));
env::remove_var("TAP_HOME");
if let Some(val) = old_home {
env::set_var("TAP_HOME", val);
}
if let Some(val) = old_test {
env::set_var("TAP_TEST_DIR", val);
}
}
#[test]
#[serial]
fn test_storage_persistence_with_temp_dir() {
use crate::test_utils::TestStorage;
let test_storage = TestStorage::new().unwrap();
let mut storage = KeyStorage::new();
storage.add_key(StoredKey {
did: "did:key:test".to_string(),
label: "test-key".to_string(),
key_type: KeyType::Ed25519,
private_key: "test-private".to_string(),
public_key: "test-public".to_string(),
metadata: HashMap::new(),
});
test_storage.save(&storage).unwrap();
assert!(
test_storage.path().exists(),
"Keys file should exist at: {:?}",
test_storage.path()
);
let loaded = test_storage.load().unwrap();
assert_eq!(
loaded.keys.len(),
1,
"Should have exactly 1 key in loaded storage"
);
assert!(
loaded.keys.contains_key("did:key:test"),
"Should contain the test key"
);
}
#[cfg(unix)]
#[test]
#[serial]
fn test_key_storage_file_permissions() {
use crate::test_utils::TestStorage;
use std::os::unix::fs::PermissionsExt;
let test_storage = TestStorage::new().unwrap();
let mut storage = KeyStorage::new();
storage.add_key(StoredKey {
did: "did:key:test".to_string(),
label: "test-key".to_string(),
key_type: KeyType::Ed25519,
private_key: "test-private-key-material".to_string(),
public_key: "test-public".to_string(),
metadata: HashMap::new(),
});
test_storage.save(&storage).unwrap();
let metadata = fs::metadata(test_storage.path()).unwrap();
let permissions = metadata.permissions();
let mode = permissions.mode() & 0o777;
assert_eq!(
mode, 0o600,
"Key storage file should have permissions 0o600 (owner read/write only), got {:o}",
mode
);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct KeyStorage {
pub keys: HashMap<String, StoredKey>,
pub default_did: Option<String>,
#[serde(default = "chrono::Utc::now")]
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(default = "chrono::Utc::now")]
pub updated_at: chrono::DateTime<chrono::Utc>,
#[serde(skip)]
base_directory: Option<PathBuf>,
}
impl KeyStorage {
pub fn new() -> Self {
Default::default()
}
pub fn add_key(&mut self, mut key: StoredKey) {
if key.label.is_empty() {
key.label = self.generate_default_label();
}
let final_label = self.ensure_unique_label(&key.label, Some(&key.did));
key.label = final_label.clone();
if self.keys.is_empty() {
self.default_did = Some(key.did.clone());
}
self.keys.insert(key.did.clone(), key);
self.updated_at = chrono::Utc::now();
}
fn generate_default_label(&self) -> String {
let mut counter = 1;
loop {
let label = format!("agent-{}", counter);
if !self.keys.values().any(|key| key.label == label) {
return label;
}
counter += 1;
}
}
fn ensure_unique_label(&self, desired_label: &str, exclude_did: Option<&str>) -> String {
if let Some(existing_key) = self.keys.values().find(|key| key.label == desired_label) {
if exclude_did.is_some() && existing_key.did == exclude_did.unwrap() {
return desired_label.to_string();
}
} else {
return desired_label.to_string();
}
let mut counter = 2;
loop {
let new_label = format!("{}-{}", desired_label, counter);
if !self.keys.values().any(|key| key.label == new_label) {
return new_label;
}
counter += 1;
}
}
pub fn find_by_label(&self, label: &str) -> Option<&StoredKey> {
self.keys.values().find(|key| key.label == label)
}
pub fn update_label(&mut self, did: &str, new_label: &str) -> Result<()> {
if !self.keys.contains_key(did) {
return Err(Error::Storage(format!("Key with DID '{}' not found", did)));
}
let final_label = self.ensure_unique_label(new_label, Some(did));
if let Some(key) = self.keys.get_mut(did) {
key.label = final_label;
}
self.updated_at = chrono::Utc::now();
Ok(())
}
pub fn default_key_path() -> Option<PathBuf> {
if let Ok(tap_home) = env::var("TAP_HOME") {
return Some(PathBuf::from(tap_home).join(DEFAULT_KEYS_FILE));
}
if let Ok(test_dir) = env::var("TAP_TEST_DIR") {
return Some(
PathBuf::from(test_dir)
.join(DEFAULT_TAP_DIR)
.join(DEFAULT_KEYS_FILE),
);
}
home_dir().map(|home| home.join(DEFAULT_TAP_DIR).join(DEFAULT_KEYS_FILE))
}
pub fn load_default() -> Result<Self> {
let path = Self::default_key_path().ok_or_else(|| {
Error::Storage("Could not determine home directory for default key path".to_string())
})?;
Self::load_from_path(&path)
}
pub fn load_from_path(path: &Path) -> Result<Self> {
let mut storage = if !path.exists() {
Self::new()
} else {
let contents = fs::read_to_string(path)
.map_err(|e| Error::Storage(format!("Failed to read key storage file: {}", e)))?;
let mut storage: KeyStorage = serde_json::from_str(&contents)
.map_err(|e| Error::Storage(format!("Failed to parse key storage file: {}", e)))?;
storage.ensure_all_keys_have_labels();
storage
};
if let Some(parent) = path.parent() {
storage.base_directory = Some(parent.to_path_buf());
}
Ok(storage)
}
fn ensure_all_keys_have_labels(&mut self) {
let mut keys_to_update = Vec::new();
for (did, key) in &self.keys {
if key.label.is_empty() {
keys_to_update.push(did.clone());
}
}
for did in keys_to_update {
let new_label = self.generate_default_label();
if let Some(key) = self.keys.get_mut(&did) {
key.label = new_label;
}
}
}
pub fn save_default(&self) -> Result<()> {
let path = Self::default_key_path().ok_or_else(|| {
Error::Storage("Could not determine home directory for default key path".to_string())
})?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
Error::Storage(format!("Failed to create key storage directory: {}", e))
})?;
}
self.save_to_path(&path)
}
pub fn save_to_path(&self, path: &Path) -> Result<()> {
let contents = serde_json::to_string_pretty(self)
.map_err(|e| Error::Storage(format!("Failed to serialize key storage: {}", e)))?;
fs::write(path, &contents)
.map_err(|e| Error::Storage(format!("Failed to write key storage file: {}", e)))?;
set_secure_file_permissions(path)?;
Ok(())
}
pub fn from_generated_key(key: &GeneratedKey) -> StoredKey {
StoredKey {
did: key.did.clone(),
label: String::new(), key_type: key.key_type,
private_key: base64::engine::general_purpose::STANDARD.encode(&key.private_key),
public_key: base64::engine::general_purpose::STANDARD.encode(&key.public_key),
metadata: HashMap::new(),
}
}
pub fn from_generated_key_with_label(key: &GeneratedKey, label: &str) -> StoredKey {
StoredKey {
did: key.did.clone(),
label: label.to_string(),
key_type: key.key_type,
private_key: base64::engine::general_purpose::STANDARD.encode(&key.private_key),
public_key: base64::engine::general_purpose::STANDARD.encode(&key.public_key),
metadata: HashMap::new(),
}
}
pub fn to_secret(key: &StoredKey) -> Secret {
Secret {
id: key.did.clone(),
type_: SecretType::JsonWebKey2020,
secret_material: SecretMaterial::JWK {
private_key_jwk: generate_jwk_for_key(key),
},
}
}
pub fn create_agent_directory(
&self,
did: &str,
policies: &[String],
metadata: &HashMap<String, String>,
) -> Result<()> {
let sanitized_did = sanitize_did(did);
let agent_dir = self.get_agent_directory(&sanitized_did)?;
fs::create_dir_all(&agent_dir).map_err(|e| {
Error::Storage(format!(
"Failed to create agent directory {}: {}",
agent_dir.display(),
e
))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let dir_permissions = fs::Permissions::from_mode(0o700);
fs::set_permissions(&agent_dir, dir_permissions).map_err(|e| {
Error::Storage(format!("Failed to set agent directory permissions: {}", e))
})?;
}
let policies_file = agent_dir.join("policies.json");
let policies_json = serde_json::to_string_pretty(policies)
.map_err(|e| Error::Storage(format!("Failed to serialize policies: {}", e)))?;
fs::write(&policies_file, &policies_json)
.map_err(|e| Error::Storage(format!("Failed to write policies file: {}", e)))?;
set_secure_file_permissions(&policies_file)?;
let metadata_file = agent_dir.join("metadata.json");
let metadata_json = serde_json::to_string_pretty(metadata)
.map_err(|e| Error::Storage(format!("Failed to serialize metadata: {}", e)))?;
fs::write(&metadata_file, &metadata_json)
.map_err(|e| Error::Storage(format!("Failed to write metadata file: {}", e)))?;
set_secure_file_permissions(&metadata_file)?;
Ok(())
}
pub fn load_agent_policies(&self, did: &str) -> Result<Vec<String>> {
let sanitized_did = sanitize_did(did);
let agent_dir = self.get_agent_directory(&sanitized_did)?;
let policies_file = agent_dir.join("policies.json");
if !policies_file.exists() {
return Ok(vec![]);
}
let content = fs::read_to_string(&policies_file)
.map_err(|e| Error::Storage(format!("Failed to read policies file: {}", e)))?;
serde_json::from_str(&content)
.map_err(|e| Error::Storage(format!("Failed to parse policies file: {}", e)))
}
pub fn load_agent_metadata(&self, did: &str) -> Result<HashMap<String, String>> {
let sanitized_did = sanitize_did(did);
let agent_dir = self.get_agent_directory(&sanitized_did)?;
let metadata_file = agent_dir.join("metadata.json");
if !metadata_file.exists() {
return Ok(HashMap::new());
}
let content = fs::read_to_string(&metadata_file)
.map_err(|e| Error::Storage(format!("Failed to read metadata file: {}", e)))?;
serde_json::from_str(&content)
.map_err(|e| Error::Storage(format!("Failed to parse metadata file: {}", e)))
}
fn get_agent_directory(&self, sanitized_did: &str) -> Result<PathBuf> {
let base_dir = if let Some(ref base) = self.base_directory {
base.clone()
} else {
if let Ok(tap_home) = env::var("TAP_HOME") {
PathBuf::from(tap_home)
} else if let Ok(test_dir) = env::var("TAP_TEST_DIR") {
PathBuf::from(test_dir).join(DEFAULT_TAP_DIR)
} else {
let home = home_dir().ok_or_else(|| {
Error::Storage("Could not determine home directory".to_string())
})?;
home.join(DEFAULT_TAP_DIR)
}
};
Ok(base_dir.join(sanitized_did))
}
}
fn sanitize_did(did: &str) -> String {
did.replace(':', "_")
}
#[allow(unused_variables)]
fn set_secure_file_permissions(path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = fs::Permissions::from_mode(0o600);
fs::set_permissions(path, permissions).map_err(|e| {
Error::Storage(format!(
"Failed to set secure permissions on {}: {}",
path.display(),
e
))
})?;
}
Ok(())
}
fn generate_jwk_for_key(key: &StoredKey) -> serde_json::Value {
let kid = if key.did.starts_with("did:key:") {
let key_part = &key.did[8..]; format!("{}#{}", key.did, key_part)
} else {
format!("{}#keys-1", key.did)
};
match key.key_type {
#[cfg(feature = "crypto-ed25519")]
KeyType::Ed25519 => {
serde_json::json!({
"kty": "OKP",
"crv": "Ed25519",
"x": key.public_key,
"d": key.private_key,
"kid": kid
})
}
#[cfg(feature = "crypto-p256")]
KeyType::P256 => {
serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": key.public_key,
"d": key.private_key,
"kid": kid
})
}
#[cfg(feature = "crypto-secp256k1")]
KeyType::Secp256k1 => {
serde_json::json!({
"kty": "EC",
"crv": "secp256k1",
"x": key.public_key,
"d": key.private_key,
"kid": kid
})
}
}
}