Files
nx9-wg/crates/nx9-wg-core/src/crypto.rs
T
2026-09-02 15:19:19 +05:30

153 lines
5.1 KiB
Rust

//! 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<String> {
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<bool> {
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<WireGuardPublicKey> {
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::<String>();
(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);
}
}