use std::time::{SystemTime, UNIX_EPOCH}; use aes_gcm::aead::{Aead, KeyInit}; use aes_gcm::{Aes256Gcm, Nonce}; use base64::Engine; use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE_NO_PAD}; use rand::RngExt; use rand::distr::{Alphanumeric, SampleString}; use rand_core::{OsRng, RngCore}; use sha2::{Digest, Sha256}; use crate::errors::{AuthError, AuthResult}; const NONCE_LEN: usize = 12; pub fn now_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|elapsed| elapsed.as_secs()) .unwrap_or_default() } pub fn random_bytes(len: usize) -> Vec { let mut bytes = vec![0u8; len]; // 密钥材料统一走 OS 熵源;ThreadRng 仅用于非安全场景。 OsRng.fill_bytes(&mut bytes); bytes } pub fn random_alnum(len: usize) -> String { Alphanumeric.sample_string(&mut rand::rng(), len) } pub fn random_b64url(len: usize) -> String { URL_SAFE_NO_PAD.encode(random_bytes(len)) } pub fn random_numeric_code(len: usize) -> String { let mut rng = rand::rng(); (0..len) .map(|_| char::from_digit(rng.random_range(0..10u32), 10).unwrap_or('0')) .collect() } pub fn to_hex(bytes: &[u8]) -> String { let mut out = String::with_capacity(bytes.len() * 2); for byte in bytes { out.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0')); out.push(char::from_digit((byte & 0x0f) as u32, 16).unwrap_or('0')); } out } pub fn sha256(bytes: &[u8]) -> [u8; 32] { Sha256::digest(bytes).into() } pub fn sha256_hex(bytes: &[u8]) -> String { to_hex(&sha256(bytes)) } /// OpenSSH fingerprints use standard Base64 without padding. pub fn sha256_b64_standard(bytes: &[u8]) -> String { STANDARD_NO_PAD.encode(sha256(bytes)) } pub fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { crypt::constant_time_eq(left, right) } pub fn decode_base64_key(material: &str) -> AuthResult> { let trimmed = material.trim(); if trimmed.is_empty() { return Err(AuthError::internal("encryption key is not configured")); } STANDARD .decode(trimmed) .or_else(|_| URL_SAFE_NO_PAD.decode(trimmed)) .map_err(|err| AuthError::internal(format!("encryption key is not valid base64: {err}"))) } fn derive_key(material: &[u8], purpose: &str) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(b"auth.kdf.v1\n"); hasher.update((material.len() as u64).to_be_bytes()); hasher.update(material); hasher.update(b"\n"); hasher.update(purpose.as_bytes()); hasher.finalize().into() } pub fn aes_encrypt(material: &[u8], purpose: &str, plaintext: &str) -> AuthResult { let key = derive_key(material, purpose); let cipher = Aes256Gcm::new_from_slice(&key) .map_err(|_| AuthError::internal("invalid encryption key length"))?; let nonce_bytes = random_bytes(NONCE_LEN); let nonce = Nonce::try_from(nonce_bytes.as_slice()) .map_err(|_| AuthError::internal("invalid nonce"))?; let sealed = cipher .encrypt(&nonce, plaintext.as_bytes()) .map_err(|_| AuthError::internal("encryption failed"))?; let mut out = Vec::with_capacity(NONCE_LEN + sealed.len()); out.extend_from_slice(&nonce_bytes); out.extend_from_slice(&sealed); Ok(URL_SAFE_NO_PAD.encode(out)) } pub fn aes_decrypt(material: &[u8], purpose: &str, stored: &str) -> AuthResult { let trimmed = stored.trim(); let raw = URL_SAFE_NO_PAD .decode(trimmed) .or_else(|_| STANDARD.decode(trimmed)) .map_err(|err| AuthError::internal(format!("ciphertext is not valid base64: {err}")))?; if raw.len() <= NONCE_LEN { return Err(AuthError::internal("ciphertext is truncated")); } let (nonce_bytes, sealed) = raw.split_at(NONCE_LEN); let key = derive_key(material, purpose); let cipher = Aes256Gcm::new_from_slice(&key) .map_err(|_| AuthError::internal("invalid encryption key length"))?; let nonce = Nonce::try_from(nonce_bytes).map_err(|_| AuthError::internal("invalid nonce"))?; let plaintext = cipher .decrypt(&nonce, sealed) .map_err(|_| AuthError::internal("decryption failed"))?; String::from_utf8(plaintext) .map_err(|err| AuthError::internal(format!("plaintext is not utf8: {err}"))) }