use chacha20poly1305::aead::{Aead, KeyInit};
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
use hkdf::Hkdf;
use rand::RngCore;
use sha2::Sha256;
use zeroize::{Zeroize, Zeroizing};
use anima_core::error::{AnimaError, AnimaResult};
const ED25519_DOMAIN: &[u8] = b"anima/ed25519/v1";
const SECP256K1_DOMAIN: &[u8] = b"anima/secp256k1/v1";
#[derive(Zeroize)]
#[zeroize(drop)]
pub struct MasterSeed {
bytes: [u8; 32],
}
impl MasterSeed {
pub fn generate() -> Self {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
Self { bytes }
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self { bytes }
}
pub fn derive_ed25519_key(&self) -> Zeroizing<[u8; 32]> {
let hk = Hkdf::<Sha256>::new(None, &self.bytes);
let mut okm = Zeroizing::new([0u8; 32]);
hk.expand(ED25519_DOMAIN, okm.as_mut())
.expect("HKDF expand should not fail for 32-byte output");
okm
}
pub fn derive_secp256k1_key(&self) -> Zeroizing<[u8; 32]> {
let hk = Hkdf::<Sha256>::new(None, &self.bytes);
let mut okm = Zeroizing::new([0u8; 32]);
hk.expand(SECP256K1_DOMAIN, okm.as_mut())
.expect("HKDF expand should not fail for 32-byte output");
okm
}
pub fn encrypt(&self, encryption_key: &[u8; 32]) -> AnimaResult<EncryptedSeed> {
let cipher = ChaCha20Poly1305::new(encryption_key.into());
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, self.bytes.as_ref())
.map_err(|e| AnimaError::Crypto(format!("seed encryption failed: {e}")))?;
Ok(EncryptedSeed {
nonce: nonce_bytes.to_vec(),
ciphertext,
})
}
pub fn decrypt(encrypted: &EncryptedSeed, encryption_key: &[u8; 32]) -> AnimaResult<Self> {
let cipher = ChaCha20Poly1305::new(encryption_key.into());
let nonce = Nonce::from_slice(&encrypted.nonce);
let plaintext = cipher
.decrypt(nonce, encrypted.ciphertext.as_ref())
.map_err(|e| AnimaError::Crypto(format!("seed decryption failed: {e}")))?;
if plaintext.len() != 32 {
return Err(AnimaError::Crypto(format!(
"decrypted seed has wrong length: {} (expected 32)",
plaintext.len()
)));
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&plaintext);
Ok(Self { bytes })
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.bytes
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EncryptedSeed {
pub nonce: Vec<u8>,
pub ciphertext: Vec<u8>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derived_keys_are_different() {
let seed = MasterSeed::generate();
let ed25519_key = seed.derive_ed25519_key();
let secp256k1_key = seed.derive_secp256k1_key();
assert_ne!(ed25519_key.as_ref(), secp256k1_key.as_ref());
}
#[test]
fn derivation_is_deterministic() {
let bytes = [42u8; 32];
let seed1 = MasterSeed::from_bytes(bytes);
let seed2 = MasterSeed::from_bytes(bytes);
assert_eq!(
seed1.derive_ed25519_key().as_ref(),
seed2.derive_ed25519_key().as_ref()
);
assert_eq!(
seed1.derive_secp256k1_key().as_ref(),
seed2.derive_secp256k1_key().as_ref()
);
}
#[test]
fn different_seeds_produce_different_keys() {
let seed1 = MasterSeed::from_bytes([1u8; 32]);
let seed2 = MasterSeed::from_bytes([2u8; 32]);
assert_ne!(
seed1.derive_ed25519_key().as_ref(),
seed2.derive_ed25519_key().as_ref()
);
}
#[test]
fn encrypt_decrypt_roundtrip() {
let seed = MasterSeed::generate();
let original_ed25519 = seed.derive_ed25519_key().to_vec();
let encryption_key = [99u8; 32];
let encrypted = seed.encrypt(&encryption_key).unwrap();
let decrypted = MasterSeed::decrypt(&encrypted, &encryption_key).unwrap();
let recovered_ed25519 = decrypted.derive_ed25519_key().to_vec();
assert_eq!(original_ed25519, recovered_ed25519);
}
#[test]
fn wrong_key_fails_decryption() {
let seed = MasterSeed::generate();
let encryption_key = [99u8; 32];
let encrypted = seed.encrypt(&encryption_key).unwrap();
let wrong_key = [100u8; 32];
assert!(MasterSeed::decrypt(&encrypted, &wrong_key).is_err());
}
}