153 lines
5.1 KiB
Rust
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);
|
|
}
|
|
}
|