//! 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 //! - Bech32 address encoding use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Argon2, Params, }; use bip39::{Language, Mnemonic}; use chacha20poly1305::{ aead::{Aead, KeyInit}, ChaCha20Poly1305, Nonce, }; use hmac::{Hmac, Mac}; use rand::RngCore; use sha2::Sha512; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{Error, Result}; /// Encrypted wallet data stored on disk #[derive(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 mnemonic = Mnemonic::generate_in(Language::English, 24) .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_in(Language::English, phrase) .map_err(|e| 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_in(Language::English, 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 type HmacSha512 = Hmac; let mut mac = HmacSha512::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::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 sha2::{Digest, Sha256}; // Hash the public key (SHA256 then take first 20 bytes like Bitcoin) let hash = Sha256::digest(pubkey); let pubkey_hash: Vec = hash[..20].to_vec(); // Convert to 5-bit groups for bech32 let mut data5 = Vec::with_capacity(33); data5.push(bech32::u5::try_from_u8(0).unwrap()); // version byte // Convert 8-bit to 5-bit let mut acc = 0u32; let mut bits = 0u32; for byte in &pubkey_hash { acc = (acc << 8) | (*byte as u32); bits += 8; while bits >= 5 { bits -= 5; data5.push(bech32::u5::try_from_u8(((acc >> bits) & 31) as u8).unwrap()); } } if bits > 0 { data5.push(bech32::u5::try_from_u8(((acc << (5 - bits)) & 31) as u8).unwrap()); } let hrp = if testnet { "tsynor" } else { "synor" }; bech32::encode(hrp, data5, bech32::Variant::Bech32) .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")); } }