//! Cryptographic utilities. use crate::error::{Nx9Error, Result}; use crate::types::wireguard::{WireGuardPresharedKey, WireGuardPrivateKey, WireGuardPublicKey}; /// Hash a password with Argon2id. Returns the PHC-formatted hash string. pub fn hash_password(password: &str) -> Result { use argon2::Argon2; use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::{PasswordHasher, SaltString}; let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); argon2 .hash_password(password.as_bytes(), &salt) .map(|h| h.to_string()) .map_err(|e| Nx9Error::Crypto(format!("password hashing failed: {}", e))) } /// Verify a password against an Argon2id PHC hash string. pub fn verify_password(password: &str, hash: &str) -> Result { use argon2::Argon2; use argon2::password_hash::{PasswordHash, PasswordVerifier}; let parsed_hash = PasswordHash::new(hash) .map_err(|e| Nx9Error::Crypto(format!("invalid password hash: {}", e)))?; Ok(Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) .is_ok()) } /// Generate a WireGuard key pair (x25519). pub fn generate_keypair() -> (WireGuardPrivateKey, WireGuardPublicKey) { use base64::Engine; use base64::engine::general_purpose::STANDARD; use rand::rngs::OsRng; use x25519_dalek::{PublicKey, StaticSecret}; let secret = StaticSecret::random_from_rng(OsRng); let public = PublicKey::from(&secret); let priv_b64 = STANDARD.encode(secret.to_bytes()); let pub_b64 = STANDARD.encode(public.as_bytes()); ( WireGuardPrivateKey::new(priv_b64), WireGuardPublicKey::new(pub_b64), ) } /// Derive a WireGuard public key (x25519) from a base64-encoded private key. pub fn derive_public_key(private_key_b64: &str) -> Result { use base64::Engine; use base64::engine::general_purpose::STANDARD; use x25519_dalek::{PublicKey, StaticSecret}; let key_bytes = STANDARD .decode(private_key_b64.trim()) .map_err(|e| Nx9Error::Validation(format!("invalid base64 private key: {e}")))?; if key_bytes.len() != 32 { return Err(Nx9Error::Validation( "private key must be exactly 32 bytes (256 bits)".to_string(), )); } let mut bytes = [0u8; 32]; bytes.copy_from_slice(&key_bytes); let secret = StaticSecret::from(bytes); let public = PublicKey::from(&secret); Ok(WireGuardPublicKey::new(STANDARD.encode(public.as_bytes()))) } /// Generate a WireGuard preshared key (32 random bytes, base64). pub fn generate_preshared_key() -> WireGuardPresharedKey { use base64::Engine; use base64::engine::general_purpose::STANDARD; use rand::RngCore; let mut key = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut key); WireGuardPresharedKey::new(STANDARD.encode(key)) } /// Generate a session ID (UUID v4). pub fn generate_session_id() -> String { uuid::Uuid::new_v4().to_string() } /// Generate an API token. Returns (plaintext_token, sha256_hex_hash). pub fn generate_api_token() -> (String, String) { use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use rand::RngCore; use sha2::{Digest, Sha256}; let mut token_bytes = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut token_bytes); let plaintext = format!("nx9_{}", URL_SAFE_NO_PAD.encode(token_bytes)); let hash_bytes = Sha256::digest(plaintext.as_bytes()); let hash = hash_bytes .iter() .map(|b| format!("{:02x}", b)) .collect::(); (plaintext, hash) } /// Generate a cryptographically secure random password. pub fn generate_secure_password(length: usize) -> String { use rand::Rng; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*-_=+"; let mut rng = rand::rngs::OsRng; (0..length) .map(|_| { let idx = rng.gen_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_password_hashing() { let password = "my_secure_password"; let hash = hash_password(password).unwrap(); assert!(verify_password(password, &hash).unwrap()); assert!(!verify_password("wrong_password", &hash).unwrap()); } #[test] fn test_generate_keypair() { let (priv_key, pub_key) = generate_keypair(); assert!(!priv_key.as_str().is_empty()); assert!(!pub_key.as_str().is_empty()); let derived_pub = derive_public_key(priv_key.as_str()).unwrap(); assert_eq!(derived_pub.as_str(), pub_key.as_str()); } #[test] fn test_derive_public_key_invalid() { assert!(derive_public_key("not-base64!").is_err()); assert!(derive_public_key("dG9vLXNob3J0").is_err()); } #[test] fn test_generate_api_token() { let (token, hash) = generate_api_token(); assert!(token.starts_with("nx9_")); assert_eq!(hash.len(), 64); } #[test] fn test_generate_secure_password() { let pw = generate_secure_password(16); assert_eq!(pw.len(), 16); } }