auth.rs (3450B)
1 use anyhow::{Context, Result, bail}; 2 use getrandom::getrandom; 3 use pbkdf2::pbkdf2_hmac; 4 use sha2::{Digest, Sha256}; 5 6 const PASSWORD_KDF_ALGORITHM: &str = "pbkdf2-sha256"; 7 const PASSWORD_KDF_ITERATIONS: u32 = 210_000; 8 9 pub(super) fn validate_password(password: &str) -> Result<()> { 10 if password.len() < 12 { 11 bail!("password must be at least 12 characters"); 12 } 13 if password.len() > 1024 { 14 bail!("password is too long"); 15 } 16 Ok(()) 17 } 18 19 pub(super) fn hash_password(password: &str) -> Result<String> { 20 let salt = random_bytes::<16>()?; 21 let hash = pbkdf2_sha256(password.as_bytes(), &salt, PASSWORD_KDF_ITERATIONS); 22 Ok(format!( 23 "{PASSWORD_KDF_ALGORITHM}${PASSWORD_KDF_ITERATIONS}${}${}", 24 hex_encode(salt), 25 hex_encode(hash) 26 )) 27 } 28 29 pub(super) fn verify_password(password: &str, encoded: &str) -> Result<bool> { 30 let parts = encoded.split('$').collect::<Vec<_>>(); 31 if parts.len() != 4 || parts[0] != PASSWORD_KDF_ALGORITHM { 32 bail!("unsupported password hash"); 33 } 34 let iterations = parts[1] 35 .parse::<u32>() 36 .context("invalid password hash iterations")?; 37 let salt = decode_hex(parts[2]).context("invalid password hash salt")?; 38 let expected = decode_hex(parts[3]).context("invalid password hash")?; 39 let actual = pbkdf2_sha256(password.as_bytes(), &salt, iterations); 40 Ok(constant_time_eq(&actual, &expected)) 41 } 42 43 pub(super) fn session_token_hash(token: &str) -> String { 44 hex_encode(Sha256::digest(format!("iuna-session:{token}").as_bytes())) 45 } 46 47 pub(super) fn random_hex(bytes: usize) -> Result<String> { 48 let mut value = vec![0_u8; bytes]; 49 getrandom(&mut value) 50 .map_err(|error| anyhow::anyhow!("secure random generation failed: {error}"))?; 51 Ok(hex_encode(value)) 52 } 53 54 pub(super) fn pbkdf2_sha256(password: &[u8], salt: &[u8], iterations: u32) -> [u8; 32] { 55 let mut output = [0_u8; 32]; 56 pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut output); 57 output 58 } 59 60 fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { 61 if left.len() != right.len() { 62 return false; 63 } 64 left.iter() 65 .zip(right) 66 .fold(0_u8, |diff, (left, right)| diff | (left ^ right)) 67 == 0 68 } 69 70 fn random_bytes<const N: usize>() -> Result<[u8; N]> { 71 let mut bytes = [0_u8; N]; 72 getrandom(&mut bytes) 73 .map_err(|error| anyhow::anyhow!("secure random generation failed: {error}"))?; 74 Ok(bytes) 75 } 76 77 pub(super) fn hex_encode(bytes: impl AsRef<[u8]>) -> String { 78 const HEX: &[u8; 16] = b"0123456789abcdef"; 79 let mut encoded = String::with_capacity(bytes.as_ref().len() * 2); 80 for byte in bytes.as_ref() { 81 encoded.push(HEX[(byte >> 4) as usize] as char); 82 encoded.push(HEX[(byte & 0x0f) as usize] as char); 83 } 84 encoded 85 } 86 87 fn decode_hex(input: &str) -> Result<Vec<u8>> { 88 if input.len() % 2 != 0 { 89 bail!("hex string has odd length"); 90 } 91 let mut bytes = Vec::with_capacity(input.len() / 2); 92 for pair in input.as_bytes().chunks_exact(2) { 93 let high = decode_hex_nibble(pair[0])?; 94 let low = decode_hex_nibble(pair[1])?; 95 bytes.push((high << 4) | low); 96 } 97 Ok(bytes) 98 } 99 100 fn decode_hex_nibble(byte: u8) -> Result<u8> { 101 match byte { 102 b'0'..=b'9' => Ok(byte - b'0'), 103 b'a'..=b'f' => Ok(byte - b'a' + 10), 104 b'A'..=b'F' => Ok(byte - b'A' + 10), 105 _ => bail!("invalid hex character"), 106 } 107 }