//! Cryptographic operations for the Synor wallet //! //! Implements: //! - BIP39 mnemonic generation and validation //! - Argon2id password-based key derivation //! - ChaCha20-Poly1305 authenticated encryption //! - Ed25519 key derivation from seed //! - Bech32m address encoding use argon2::{ password_hash::SaltString, Argon2, Params, }; use bip39::Mnemonic; use chacha20poly1305::{ aead::{Aead, KeyInit}, ChaCha20Poly1305, Nonce, }; use hmac::Mac; use rand::{rngs::OsRng, RngCore}; use sha2::Sha512; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{Error, Result}; /// Type alias for HMAC-SHA512 to avoid ambiguity type HmacSha512 = hmac::Hmac; /// Encrypted wallet data stored on disk #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct EncryptedWallet { /// Argon2 salt (22 bytes, base64 encoded) pub salt: String, /// ChaCha20-Poly1305 nonce (12 bytes, hex encoded) pub nonce: String, /// Encrypted seed (ciphertext + 16-byte tag, hex encoded) pub ciphertext: String, /// Version for future compatibility pub version: u32, } /// Sensitive seed data that auto-zeros on drop #[derive(Zeroize, ZeroizeOnDrop)] pub struct SeedData { /// 64-byte seed derived from mnemonic pub seed: [u8; 64], } /// Generate a new random 24-word BIP39 mnemonic pub fn generate_mnemonic() -> Result { // Generate 256 bits of entropy for 24 words let mut entropy = [0u8; 32]; // 256 bits for 24 words OsRng.fill_bytes(&mut entropy); let mnemonic = Mnemonic::from_entropy(&entropy) .map_err(|e| Error::Crypto(format!("Failed to generate mnemonic: {}", e)))?; Ok(mnemonic.to_string()) } /// Validate a BIP39 mnemonic phrase pub fn validate_mnemonic(phrase: &str) -> Result<()> { Mnemonic::parse(phrase) .map_err(|_| Error::InvalidMnemonic)?; Ok(()) } /// Derive a 64-byte seed from mnemonic using BIP39 /// The passphrase is optional (empty string if not used) pub fn mnemonic_to_seed(mnemonic: &str, passphrase: &str) -> Result { let mnemonic = Mnemonic::parse(mnemonic) .map_err(|_| Error::InvalidMnemonic)?; let seed = mnemonic.to_seed(passphrase); let mut seed_array = [0u8; 64]; seed_array.copy_from_slice(&seed); Ok(SeedData { seed: seed_array }) } /// Derive an encryption key from password using Argon2id fn derive_encryption_key(password: &str, salt: &SaltString) -> Result<[u8; 32]> { // Use Argon2id with secure parameters // Memory: 64 MB, Iterations: 3, Parallelism: 4 let params = Params::new(65536, 3, 4, Some(32)) .map_err(|e| Error::Crypto(format!("Argon2 params error: {}", e)))?; let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params); let mut key = [0u8; 32]; argon2 .hash_password_into(password.as_bytes(), salt.as_str().as_bytes(), &mut key) .map_err(|e| Error::Crypto(format!("Key derivation failed: {}", e)))?; Ok(key) } /// Encrypt the seed with password using Argon2id + ChaCha20-Poly1305 pub fn encrypt_seed(seed: &[u8; 64], password: &str) -> Result { // Generate random salt for Argon2 let salt = SaltString::generate(&mut OsRng); // Derive encryption key let key = derive_encryption_key(password, &salt)?; // Generate random nonce for ChaCha20-Poly1305 let mut nonce_bytes = [0u8; 12]; OsRng.fill_bytes(&mut nonce_bytes); let nonce = Nonce::from_slice(&nonce_bytes); // Encrypt the seed let cipher = ChaCha20Poly1305::new_from_slice(&key) .map_err(|e| Error::Crypto(format!("Cipher init failed: {}", e)))?; let ciphertext = cipher .encrypt(nonce, seed.as_ref()) .map_err(|e| Error::Crypto(format!("Encryption failed: {}", e)))?; Ok(EncryptedWallet { salt: salt.to_string(), nonce: hex::encode(nonce_bytes), ciphertext: hex::encode(ciphertext), version: 1, }) } /// Decrypt the seed with password pub fn decrypt_seed(wallet: &EncryptedWallet, password: &str) -> Result { if wallet.version != 1 { return Err(Error::Crypto(format!( "Unsupported wallet version: {}", wallet.version ))); } // Parse salt let salt = SaltString::from_b64(&wallet.salt) .map_err(|_| Error::Crypto("Invalid salt".to_string()))?; // Derive encryption key let key = derive_encryption_key(password, &salt)?; // Parse nonce let nonce_bytes = hex::decode(&wallet.nonce) .map_err(|_| Error::Crypto("Invalid nonce".to_string()))?; if nonce_bytes.len() != 12 { return Err(Error::Crypto("Invalid nonce length".to_string())); } let nonce = Nonce::from_slice(&nonce_bytes); // Parse ciphertext let ciphertext = hex::decode(&wallet.ciphertext) .map_err(|_| Error::Crypto("Invalid ciphertext".to_string()))?; // Decrypt let cipher = ChaCha20Poly1305::new_from_slice(&key) .map_err(|e| Error::Crypto(format!("Cipher init failed: {}", e)))?; let plaintext = cipher .decrypt(nonce, ciphertext.as_ref()) .map_err(|_| Error::InvalidPassword)?; if plaintext.len() != 64 { return Err(Error::Crypto("Invalid seed length".to_string())); } let mut seed = [0u8; 64]; seed.copy_from_slice(&plaintext); Ok(SeedData { seed }) } /// Derive an Ed25519 keypair from seed using BIP32-Ed25519 style derivation /// Returns (private_key, public_key) pub fn derive_ed25519_keypair(seed: &[u8; 64], index: u32) -> ([u8; 32], [u8; 32]) { // Use HMAC-SHA512 for deterministic key derivation // Path: m/44'/synor'/0'/0/index let mut mac: HmacSha512 = Mac::new_from_slice(b"ed25519 seed").unwrap(); mac.update(seed); let master = mac.finalize().into_bytes(); // Derive child key for index let mut mac: HmacSha512 = Mac::new_from_slice(&master[32..]).unwrap(); mac.update(&master[..32]); mac.update(&index.to_be_bytes()); let derived = mac.finalize().into_bytes(); let mut private_key = [0u8; 32]; let mut chain_code = [0u8; 32]; private_key.copy_from_slice(&derived[..32]); chain_code.copy_from_slice(&derived[32..]); // For Ed25519, the public key would normally be derived using the ed25519 library // Here we return the chain_code as a placeholder for the public key // In production, use: ed25519_dalek::SigningKey::from_bytes(&private_key).verifying_key() (private_key, chain_code) } /// Generate a Synor address from a public key /// Format: synor1 pub fn pubkey_to_address(pubkey: &[u8; 32], testnet: bool) -> Result { use bech32::Hrp; use sha2::{Digest, Sha256}; // Hash the public key (SHA256 then take first 20 bytes like Bitcoin) let hash = Sha256::digest(pubkey); let pubkey_hash: &[u8] = &hash[..20]; let hrp_str = if testnet { "tsynor" } else { "synor" }; let hrp = Hrp::parse(hrp_str) .map_err(|e| Error::Crypto(format!("Invalid HRP: {}", e)))?; // Encode using bech32 segwit encoding (version 0) bech32::segwit::encode(hrp, bech32::segwit::VERSION_0, pubkey_hash) .map_err(|e| Error::Crypto(format!("Bech32 encoding failed: {}", e))) } #[cfg(test)] mod tests { use super::*; #[test] fn test_mnemonic_generation() { let mnemonic = generate_mnemonic().unwrap(); let words: Vec<&str> = mnemonic.split_whitespace().collect(); assert_eq!(words.len(), 24); assert!(validate_mnemonic(&mnemonic).is_ok()); } #[test] fn test_encrypt_decrypt_roundtrip() { let seed = [42u8; 64]; let password = "test_password_123"; let encrypted = encrypt_seed(&seed, password).unwrap(); let decrypted = decrypt_seed(&encrypted, password).unwrap(); assert_eq!(seed, decrypted.seed); } #[test] fn test_wrong_password_fails() { let seed = [42u8; 64]; let encrypted = encrypt_seed(&seed, "correct_password").unwrap(); let result = decrypt_seed(&encrypted, "wrong_password"); assert!(result.is_err()); } #[test] fn test_address_generation() { let pubkey = [1u8; 32]; let address = pubkey_to_address(&pubkey, false).unwrap(); assert!(address.starts_with("synor1")); let testnet_address = pubkey_to_address(&pubkey, true).unwrap(); assert!(testnet_address.starts_with("tsynor1")); } }