use anima_core::error::{AnimaError, AnimaResult};
use anima_core::identity::{AgentIdentity, LifecycleState};
use chrono::Utc;
use haima_core::wallet::{ChainId, WalletAddress};
use haima_wallet::evm::derive_address;
use k256::ecdsa::SigningKey as Secp256k1SigningKey;
use zeroize::Zeroizing;
use crate::ed25519::Ed25519Identity;
use crate::seed::{EncryptedSeed, MasterSeed};
pub struct AnimaKeystore {
seed: MasterSeed,
ed25519: Ed25519Identity,
secp256k1_key: Zeroizing<Vec<u8>>,
wallet_address: WalletAddress,
}
impl AnimaKeystore {
pub fn generate() -> AnimaResult<Self> {
let seed = MasterSeed::generate();
Self::from_seed(seed)
}
pub fn from_seed(seed: MasterSeed) -> AnimaResult<Self> {
let ed25519_key = seed.derive_ed25519_key();
let ed25519 = Ed25519Identity::from_key_bytes(&ed25519_key)?;
let secp256k1_bytes = seed.derive_secp256k1_key();
let secp256k1_signing = Secp256k1SigningKey::from_bytes(secp256k1_bytes.as_ref().into())
.map_err(|e| AnimaError::Crypto(format!("secp256k1 key derivation: {e}")))?;
let address = derive_address(&secp256k1_signing);
let wallet_address = WalletAddress {
address,
chain: ChainId::base(),
};
Ok(Self {
seed,
ed25519,
secp256k1_key: Zeroizing::new(secp256k1_bytes.to_vec()),
wallet_address,
})
}
pub fn from_encrypted(
encrypted: &EncryptedSeed,
encryption_key: &[u8; 32],
) -> AnimaResult<Self> {
let seed = MasterSeed::decrypt(encrypted, encryption_key)?;
Self::from_seed(seed)
}
pub fn encrypt_seed(&self, encryption_key: &[u8; 32]) -> AnimaResult<EncryptedSeed> {
self.seed.encrypt(encryption_key)
}
pub fn ed25519(&self) -> &Ed25519Identity {
&self.ed25519
}
pub fn wallet_address(&self) -> &WalletAddress {
&self.wallet_address
}
pub fn secp256k1_key_bytes(&self) -> &Zeroizing<Vec<u8>> {
&self.secp256k1_key
}
pub fn build_identity(
&self,
agent_id: impl Into<String>,
host_id: impl Into<String>,
) -> AgentIdentity {
AgentIdentity {
agent_id: agent_id.into(),
host_id: host_id.into(),
auth_public_key: self.ed25519.public_key_bytes(),
wallet_address: self.wallet_address.clone(),
did: Some(self.ed25519.did_key()),
lifecycle: LifecycleState::Active,
created_at: Utc::now(),
expires_at: None,
seed_blob_ref: None,
}
}
pub fn sign_agent_jwt(
&self,
agent_id: &str,
audience: &str,
ttl_secs: i64,
) -> AnimaResult<String> {
self.ed25519.sign_agent_jwt(agent_id, audience, ttl_secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_keystore() {
let ks = AnimaKeystore::generate().unwrap();
assert_eq!(ks.ed25519().public_key_bytes().len(), 32);
assert!(ks.wallet_address().address.starts_with("0x"));
let identity = ks.build_identity("agt_001", "host_arcan");
assert!(identity.did.is_some());
assert!(identity.did.as_ref().unwrap().starts_with("did:key:z"));
}
#[test]
fn deterministic_from_seed() {
let bytes = [42u8; 32];
let ks1 = AnimaKeystore::from_seed(MasterSeed::from_bytes(bytes)).unwrap();
let ks2 = AnimaKeystore::from_seed(MasterSeed::from_bytes(bytes)).unwrap();
assert_eq!(
ks1.ed25519().public_key_bytes(),
ks2.ed25519().public_key_bytes()
);
assert_eq!(ks1.wallet_address().address, ks2.wallet_address().address);
}
#[test]
fn encrypt_decrypt_roundtrip() {
let ks = AnimaKeystore::generate().unwrap();
let original_pubkey = ks.ed25519().public_key_bytes();
let original_wallet = ks.wallet_address().address.clone();
let encryption_key = [77u8; 32];
let encrypted = ks.encrypt_seed(&encryption_key).unwrap();
let recovered = AnimaKeystore::from_encrypted(&encrypted, &encryption_key).unwrap();
assert_eq!(recovered.ed25519().public_key_bytes(), original_pubkey);
assert_eq!(recovered.wallet_address().address, original_wallet);
}
#[test]
fn build_identity_fields() {
let ks = AnimaKeystore::generate().unwrap();
let id = ks.build_identity("agt_test", "host_test");
assert_eq!(id.agent_id, "agt_test");
assert_eq!(id.host_id, "host_test");
assert_eq!(id.lifecycle, LifecycleState::Active);
assert_eq!(id.auth_public_key, ks.ed25519().public_key_bytes());
}
#[test]
fn sign_jwt() {
let ks = AnimaKeystore::generate().unwrap();
let jwt = ks
.sign_agent_jwt("agt_001", "https://broomva.tech", 60)
.unwrap();
assert!(!jwt.is_empty());
assert_eq!(jwt.split('.').count(), 3);
}
}