//! Key derivation functions for Synor. //! //! Provides secure key derivation using: //! - HKDF (HMAC-based Key Derivation Function) for deriving keys from high-entropy seeds //! - PBKDF2 for password-based key derivation //! - BIP-32 style hierarchical deterministic key derivation use hkdf::Hkdf; use hmac::Hmac; use pbkdf2::pbkdf2_hmac; use sha3::Sha3_256; use thiserror::Error; use zeroize::ZeroizeOnDrop; /// Domain separation tags for different key types pub mod domain { pub const ED25519_KEY: &[u8] = b"synor/ed25519/v1"; pub const DILITHIUM_KEY: &[u8] = b"synor/dilithium/v1"; pub const ENCRYPTION_KEY: &[u8] = b"synor/encryption/v1"; pub const CHILD_KEY: &[u8] = b"synor/child/v1"; } /// Derives a key from a seed using HKDF-SHA3-256. /// /// # Arguments /// * `seed` - The high-entropy input key material (IKM) /// * `salt` - Optional salt value (can be empty for most uses) /// * `info` - Context/application-specific info for domain separation /// * `output_len` - Desired output key length /// /// # Returns /// A vector of derived key bytes pub fn derive_key( seed: &[u8], salt: &[u8], info: &[u8], output_len: usize, ) -> Result { if seed.is_empty() { return Err(DeriveKeyError::EmptySeed); } if output_len == 0 || output_len > 255 * 32 { return Err(DeriveKeyError::InvalidOutputLength(output_len)); } let hk = Hkdf::::new(Some(salt), seed); let mut output = vec![0u8; output_len]; hk.expand(info, &mut output) .map_err(|_| DeriveKeyError::ExpansionFailed)?; Ok(DerivedKey(output)) } /// Derives a 32-byte key for Ed25519 signing. pub fn derive_ed25519_key(master_seed: &[u8], account: u32) -> Result { let info = [domain::ED25519_KEY, &account.to_be_bytes()].concat(); derive_key(master_seed, b"", &info, 32) } /// Derives a 32-byte seed for Dilithium key generation. pub fn derive_dilithium_seed( master_seed: &[u8], account: u32, ) -> Result { let info = [domain::DILITHIUM_KEY, &account.to_be_bytes()].concat(); derive_key(master_seed, b"", &info, 32) } /// Derives a 32-byte encryption key. pub fn derive_encryption_key( master_seed: &[u8], purpose: &[u8], ) -> Result { let info = [domain::ENCRYPTION_KEY, purpose].concat(); derive_key(master_seed, b"", &info, 32) } /// Derives a child key from a parent key using BIP-32 style derivation. pub fn derive_child_key( parent_key: &[u8], chain_code: &[u8; 32], index: u32, ) -> Result<(DerivedKey, [u8; 32]), DeriveKeyError> { use hmac::Mac; if parent_key.len() != 32 { return Err(DeriveKeyError::InvalidKeyLength); } let mut hmac = Hmac::::new_from_slice(chain_code).map_err(|_| DeriveKeyError::HmacError)?; // For hardened keys (index >= 0x80000000), use parent key // For normal keys, use parent public key (not implemented here for simplicity) if index >= 0x80000000 { hmac.update(&[0x00]); hmac.update(parent_key); } else { // Normal derivation - would need public key here // For now, treat all as hardened hmac.update(&[0x00]); hmac.update(parent_key); } hmac.update(&index.to_be_bytes()); let result = hmac.finalize().into_bytes(); // Split into child key and new chain code let mut child_key = [0u8; 32]; let mut new_chain_code = [0u8; 32]; // Use HKDF to expand to 64 bytes let hk = Hkdf::::new(None, &result); let mut expanded = [0u8; 64]; hk.expand(domain::CHILD_KEY, &mut expanded) .map_err(|_| DeriveKeyError::ExpansionFailed)?; child_key.copy_from_slice(&expanded[..32]); new_chain_code.copy_from_slice(&expanded[32..]); Ok((DerivedKey(child_key.to_vec()), new_chain_code)) } /// Derives a key from a password using PBKDF2-HMAC-SHA3-256. /// /// # Arguments /// * `password` - The password to derive from /// * `salt` - A unique salt for this derivation /// * `iterations` - Number of PBKDF2 iterations (recommended: 100,000+) /// * `output_len` - Desired output key length pub fn derive_from_password( password: &[u8], salt: &[u8], iterations: u32, output_len: usize, ) -> Result { if password.is_empty() { return Err(DeriveKeyError::EmptyPassword); } if salt.len() < 8 { return Err(DeriveKeyError::SaltTooShort); } if iterations < 10_000 { return Err(DeriveKeyError::InsufficientIterations); } if output_len == 0 || output_len > 64 { return Err(DeriveKeyError::InvalidOutputLength(output_len)); } let mut output = vec![0u8; output_len]; pbkdf2_hmac::(password, salt, iterations, &mut output); Ok(DerivedKey(output)) } /// BIP-44 style derivation path for Synor. /// Path: m/44'/synor'/account'/change/index /// Synor coin type: 0x5359 (SY in ASCII) pub struct DerivationPath { /// Account number (hardened) pub account: u32, /// Change: 0 = external, 1 = internal pub change: u32, /// Address index pub index: u32, } impl DerivationPath { /// Synor coin type for BIP-44 (0x5359 = "SY") pub const COIN_TYPE: u32 = 0x5359; /// Creates a new derivation path. pub fn new(account: u32, change: u32, index: u32) -> Self { DerivationPath { account, change, index, } } /// Creates the default path (first external address). pub fn default_external() -> Self { DerivationPath { account: 0, change: 0, index: 0, } } /// Creates a path for the nth external address. pub fn external(account: u32, index: u32) -> Self { DerivationPath { account, change: 0, index, } } /// Creates a path for the nth internal (change) address. pub fn internal(account: u32, index: u32) -> Self { DerivationPath { account, change: 1, index, } } } impl Default for DerivationPath { fn default() -> Self { Self::default_external() } } impl std::fmt::Display for DerivationPath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "m/44'/{}'/{}'/{}/{}", Self::COIN_TYPE, self.account, self.change, self.index ) } } /// A derived key that automatically zeroizes on drop. #[derive(Clone, ZeroizeOnDrop)] pub struct DerivedKey(Vec); impl DerivedKey { /// Returns the key bytes. pub fn as_bytes(&self) -> &[u8] { &self.0 } /// Consumes the key and returns the bytes. pub fn into_bytes(mut self) -> Vec { std::mem::take(&mut self.0) } /// Returns the key length. pub fn len(&self) -> usize { self.0.len() } /// Returns true if the key is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Converts to a fixed-size array. pub fn to_array(&self) -> Option<[u8; N]> { if self.0.len() != N { return None; } let mut arr = [0u8; N]; arr.copy_from_slice(&self.0); Some(arr) } } impl AsRef<[u8]> for DerivedKey { fn as_ref(&self) -> &[u8] { &self.0 } } impl std::fmt::Debug for DerivedKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "DerivedKey([{} bytes])", self.0.len()) } } /// Errors that can occur during key derivation. #[derive(Debug, Error)] pub enum DeriveKeyError { #[error("Seed cannot be empty")] EmptySeed, #[error("Password cannot be empty")] EmptyPassword, #[error("Salt must be at least 8 bytes")] SaltTooShort, #[error("At least 10,000 iterations required")] InsufficientIterations, #[error("Invalid output length: {0}")] InvalidOutputLength(usize), #[error("Invalid key length")] InvalidKeyLength, #[error("HKDF expansion failed")] ExpansionFailed, #[error("HMAC operation failed")] HmacError, } #[cfg(test)] mod tests { use super::*; #[test] fn test_derive_key() { let seed = b"test seed for key derivation"; let key = derive_key(seed, b"salt", b"info", 32).unwrap(); assert_eq!(key.len(), 32); // Same inputs should produce same output let key2 = derive_key(seed, b"salt", b"info", 32).unwrap(); assert_eq!(key.as_bytes(), key2.as_bytes()); // Different info should produce different output let key3 = derive_key(seed, b"salt", b"different", 32).unwrap(); assert_ne!(key.as_bytes(), key3.as_bytes()); } #[test] fn test_derive_ed25519_key() { let master_seed = [42u8; 64]; let key0 = derive_ed25519_key(&master_seed, 0).unwrap(); let key1 = derive_ed25519_key(&master_seed, 1).unwrap(); assert_eq!(key0.len(), 32); assert_eq!(key1.len(), 32); assert_ne!(key0.as_bytes(), key1.as_bytes()); } #[test] fn test_derive_from_password() { let password = b"correct horse battery staple"; let salt = b"unique salt value"; let key = derive_from_password(password, salt, 100_000, 32).unwrap(); assert_eq!(key.len(), 32); // Same password/salt should produce same key let key2 = derive_from_password(password, salt, 100_000, 32).unwrap(); assert_eq!(key.as_bytes(), key2.as_bytes()); // Different password should produce different key let key3 = derive_from_password(b"different password", salt, 100_000, 32).unwrap(); assert_ne!(key.as_bytes(), key3.as_bytes()); } #[test] fn test_derive_child_key() { let parent = [1u8; 32]; let chain_code = [2u8; 32]; let (child0, cc0) = derive_child_key(&parent, &chain_code, 0x80000000).unwrap(); let (child1, cc1) = derive_child_key(&parent, &chain_code, 0x80000001).unwrap(); assert_eq!(child0.len(), 32); assert_ne!(child0.as_bytes(), child1.as_bytes()); assert_ne!(cc0, cc1); } #[test] fn test_derivation_path() { let path = DerivationPath::default_external(); assert_eq!(path.to_string(), "m/44'/21337'/0'/0/0"); let path2 = DerivationPath::external(1, 5); assert_eq!(path2.to_string(), "m/44'/21337'/1'/0/5"); let path3 = DerivationPath::internal(0, 3); assert_eq!(path3.to_string(), "m/44'/21337'/0'/1/3"); } #[test] fn test_empty_seed_error() { let result = derive_key(&[], b"salt", b"info", 32); assert!(matches!(result, Err(DeriveKeyError::EmptySeed))); } #[test] fn test_password_validation() { // Empty password assert!(matches!( derive_from_password(&[], b"long enough salt", 100_000, 32), Err(DeriveKeyError::EmptyPassword) )); // Salt too short assert!(matches!( derive_from_password(b"password", b"short", 100_000, 32), Err(DeriveKeyError::SaltTooShort) )); // Not enough iterations assert!(matches!( derive_from_password(b"password", b"long enough salt", 1000, 32), Err(DeriveKeyError::InsufficientIterations) )); } }