synor/crates/synor-crypto/src/kdf.rs
Gulshan Yadav 5c643af64c fix: resolve all clippy warnings for CI
Fix all Rust clippy warnings that were causing CI failures when built
with RUSTFLAGS=-Dwarnings. Changes include:

- Replace derivable_impls with derive macros for BlockBody, Network, etc.
- Use div_ceil() instead of manual implementation
- Fix should_implement_trait by renaming from_str to parse
- Add type aliases for type_complexity warnings
- Use or_default(), is_some_and(), is_multiple_of() where appropriate
- Remove needless borrows and redundant closures
- Fix manual_strip with strip_prefix()
- Add allow attributes for intentional patterns (too_many_arguments,
  needless_range_loop in cryptographic code, assertions_on_constants)
- Remove unused imports, mut bindings, and dead code in tests
2026-01-08 05:58:22 +05:30

402 lines
11 KiB
Rust

//! 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<DerivedKey, DeriveKeyError> {
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::<Sha3_256>::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<DerivedKey, DeriveKeyError> {
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<DerivedKey, DeriveKeyError> {
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<DerivedKey, DeriveKeyError> {
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::<Sha3_256>::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::<Sha3_256>::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<DerivedKey, DeriveKeyError> {
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::<Sha3_256>(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<u8>);
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<u8> {
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<const N: usize>(&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)
));
}
}