use crate::crypto::signer::encrypt_keypair;
use crate::error::AgentError;
use crate::signing::{PassphraseProvider, SecureSigner, StorageSigner};
use crate::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
use crate::storage::memory::{MEMORY_KEYCHAIN, MemoryKeychainHandle};
use crate::crypto::provider_bridge;
use hex;
use std::path::PathBuf;
use tempfile::TempDir;
use zeroize::Zeroizing;
pub struct TestPassphraseProvider {
passphrase: String,
}
impl TestPassphraseProvider {
pub fn new(passphrase: impl Into<String>) -> Self {
Self {
passphrase: passphrase.into(),
}
}
}
impl PassphraseProvider for TestPassphraseProvider {
fn get_passphrase(&self, _prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
Ok(Zeroizing::new(self.passphrase.clone()))
}
}
pub struct TestIdentity {
pub temp_dir: TempDir,
pub identity_did: String,
pub identity_alias: KeyAlias,
pub device_aliases: Vec<String>,
pub passphrase: String,
pub identity_public_key: Vec<u8>,
pub device_public_keys: Vec<(String, Vec<u8>)>,
}
impl TestIdentity {
pub fn repo_path(&self) -> PathBuf {
self.temp_dir.path().to_path_buf()
}
pub fn signer(&self) -> impl SecureSigner {
StorageSigner::new(MemoryKeychainHandle)
}
pub fn passphrase_provider(&self) -> TestPassphraseProvider {
TestPassphraseProvider::new(&self.passphrase)
}
pub fn keychain(&self) -> Box<dyn KeyStorage + Send + Sync> {
Box::new(MemoryKeychainHandle)
}
}
pub struct TestIdentityBuilder {
alias: KeyAlias,
passphrase: String,
device_aliases: Vec<String>,
}
impl Default for TestIdentityBuilder {
fn default() -> Self {
Self::new()
}
}
impl TestIdentityBuilder {
pub fn new() -> Self {
Self {
alias: KeyAlias::new_unchecked("test-identity"),
passphrase: "Test-P@ss12345".to_string(),
device_aliases: Vec::new(),
}
}
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.alias = KeyAlias::new_unchecked(alias.into());
self
}
pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
self.passphrase = passphrase.into();
self
}
pub fn with_device(mut self, device_alias: impl Into<String>) -> Self {
self.device_aliases.push(device_alias.into());
self
}
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_types)]
pub fn build(self) -> Result<TestIdentity, AgentError> {
let temp_dir = TempDir::new().map_err(|e| {
AgentError::StorageError(format!("Failed to create temp directory: {}", e))
})?;
let output = std::process::Command::new("git")
.args(["init"])
.current_dir(temp_dir.path())
.output()
.map_err(|e| AgentError::GitError(format!("Failed to run git init: {}", e)))?;
if !output.status.success() {
return Err(AgentError::GitError(format!(
"git init failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
MEMORY_KEYCHAIN.lock().unwrap().clear_all()?;
let (identity_seed, identity_pubkey) = provider_bridge::generate_ed25519_keypair_sync()
.map_err(|e| AgentError::CryptoError(format!("Failed to generate keypair: {:?}", e)))?;
let identity_public_key = identity_pubkey.to_vec();
let did_str = format!("did:key:z6Mk{}", hex::encode(&identity_public_key));
let did = IdentityDID::new_unchecked(did_str.clone());
let identity_pkcs8 =
auths_crypto::build_ed25519_pkcs8_v2(identity_seed.as_bytes(), &identity_pubkey);
let encrypted_identity =
encrypt_keypair(&identity_pkcs8, &self.passphrase).map_err(|e| {
AgentError::CryptoError(format!("Failed to encrypt identity key: {}", e))
})?;
MEMORY_KEYCHAIN.lock().unwrap().store_key(
&self.alias,
&did,
KeyRole::Primary,
&encrypted_identity,
)?;
let mut device_public_keys = Vec::new();
for device_alias in &self.device_aliases {
let (device_seed, device_pubkey) = provider_bridge::generate_ed25519_keypair_sync()
.map_err(|e| {
AgentError::CryptoError(format!("Failed to generate device keypair: {:?}", e))
})?;
let device_pkcs8 =
auths_crypto::build_ed25519_pkcs8_v2(device_seed.as_bytes(), &device_pubkey);
let encrypted_device =
encrypt_keypair(&device_pkcs8, &self.passphrase).map_err(|e| {
AgentError::CryptoError(format!("Failed to encrypt device key: {}", e))
})?;
MEMORY_KEYCHAIN.lock().unwrap().store_key(
&KeyAlias::new_unchecked(device_alias),
&did,
KeyRole::DelegatedAgent,
&encrypted_device,
)?;
device_public_keys.push((device_alias.clone(), device_pubkey.to_vec()));
}
Ok(TestIdentity {
temp_dir,
identity_did: did_str,
identity_alias: self.alias,
device_aliases: self.device_aliases,
passphrase: self.passphrase,
identity_public_key,
device_public_keys,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use ring::signature::{ED25519, UnparsedPublicKey};
#[test]
fn test_builder_creates_identity() {
let identity = TestIdentityBuilder::new()
.with_alias("my-identity")
.with_passphrase("S3cret!Pass99")
.build()
.expect("Failed to build identity");
assert!(identity.identity_did.starts_with("did:key:z6Mk"));
assert_eq!(identity.identity_alias, "my-identity");
assert_eq!(identity.passphrase, "S3cret!Pass99");
assert!(identity.device_aliases.is_empty());
}
#[test]
fn test_builder_creates_devices() {
let identity = TestIdentityBuilder::new()
.with_device("laptop")
.with_device("phone")
.build()
.expect("Failed to build identity");
assert_eq!(identity.device_aliases.len(), 2);
assert!(identity.device_aliases.contains(&"laptop".to_string()));
assert!(identity.device_aliases.contains(&"phone".to_string()));
assert_eq!(identity.device_public_keys.len(), 2);
}
#[test]
fn test_signer_can_sign() {
let identity = TestIdentityBuilder::new()
.with_alias("signing-test")
.build()
.expect("Failed to build identity");
let signer = identity.signer();
let provider = identity.passphrase_provider();
let message = b"test message";
let signature = signer
.sign_with_alias(&identity.identity_alias, &provider, message)
.expect("Signing failed");
let public_key = UnparsedPublicKey::new(&ED25519, &identity.identity_public_key);
assert!(public_key.verify(message, &signature).is_ok());
}
#[test]
fn test_device_signer_can_sign() {
let identity = TestIdentityBuilder::new()
.with_device("test-device")
.build()
.expect("Failed to build identity");
let signer = identity.signer();
let provider = identity.passphrase_provider();
let message = b"device message";
let alias = crate::storage::keychain::KeyAlias::new_unchecked("test-device");
let signature = signer
.sign_with_alias(&alias, &provider, message)
.expect("Device signing failed");
let (_, device_pubkey) = &identity.device_public_keys[0];
let public_key = UnparsedPublicKey::new(&ED25519, device_pubkey);
assert!(public_key.verify(message, &signature).is_ok());
}
#[test]
fn test_repo_path_exists() {
let identity = TestIdentityBuilder::new()
.build()
.expect("Failed to build identity");
let repo_path = identity.repo_path();
assert!(repo_path.exists());
assert!(repo_path.join(".git").exists());
}
#[test]
fn test_passphrase_provider() {
let provider = TestPassphraseProvider::new("my-secret");
let result = provider.get_passphrase("Enter passphrase:");
assert!(result.is_ok());
assert_eq!(*result.unwrap(), "my-secret");
}
}