auth.rs (4785B)
1 use anyhow::{Context, Result, bail}; 2 use getrandom::getrandom; 3 use pbkdf2::pbkdf2_hmac; 4 use secrecy::{ExposeSecret, SecretBox, SecretString}; 5 use sha2::{Digest, Sha256}; 6 7 const PASSWORD_KDF_ALGORITHM: &str = "pbkdf2-sha256"; 8 const PASSWORD_KDF_ITERATIONS: u32 = 210_000; 9 const MIN_PASSWORD_KDF_ITERATIONS: u32 = 100_000; 10 const MAX_PASSWORD_KDF_ITERATIONS: u32 = 1_000_000; 11 12 pub(super) fn validate_password(password: &str) -> Result<()> { 13 if password.len() < 12 { 14 bail!("password must be at least 12 characters"); 15 } 16 if password.len() > 1024 { 17 bail!("password is too long"); 18 } 19 Ok(()) 20 } 21 22 pub(super) fn hash_password(password: &str) -> Result<String> { 23 let salt = random_bytes::<16>()?; 24 let hash = pbkdf2_sha256(password.as_bytes(), &salt, PASSWORD_KDF_ITERATIONS); 25 Ok(format!( 26 "{PASSWORD_KDF_ALGORITHM}${PASSWORD_KDF_ITERATIONS}${}${}", 27 hex_encode(salt), 28 hex_encode(hash.expose_secret()) 29 )) 30 } 31 32 pub(super) fn verify_password(password: &str, encoded: &str) -> Result<bool> { 33 let parts = encoded.split('$').collect::<Vec<_>>(); 34 if parts.len() != 4 || parts[0] != PASSWORD_KDF_ALGORITHM { 35 bail!("unsupported password hash"); 36 } 37 let iterations = parts[1] 38 .parse::<u32>() 39 .context("invalid password hash iterations")?; 40 validate_password_kdf_iterations(iterations)?; 41 let salt = decode_hex(parts[2]).context("invalid password hash salt")?; 42 let expected = decode_hex(parts[3]).context("invalid password hash")?; 43 let actual = pbkdf2_sha256(password.as_bytes(), &salt, iterations); 44 Ok(constant_time_eq(actual.expose_secret(), &expected)) 45 } 46 47 fn validate_password_kdf_iterations(iterations: u32) -> Result<()> { 48 if !(MIN_PASSWORD_KDF_ITERATIONS..=MAX_PASSWORD_KDF_ITERATIONS).contains(&iterations) { 49 bail!("unsupported password hash iteration count"); 50 } 51 Ok(()) 52 } 53 54 pub(super) fn session_token_hash(token: &str) -> String { 55 let mut hasher = Sha256::new(); 56 hasher.update(b"iuna-session:"); 57 hasher.update(token.as_bytes()); 58 hex_encode(hasher.finalize()) 59 } 60 61 pub(super) fn random_hex(bytes: usize) -> Result<SecretString> { 62 let mut random_error = None; 63 let value = SecretBox::<Vec<u8>>::init_with_mut(|value| { 64 value.resize(bytes, 0); 65 if let Err(error) = getrandom(value) { 66 random_error = Some(error); 67 } 68 }); 69 if let Some(error) = random_error { 70 return Err(anyhow::anyhow!("secure random generation failed: {error}")); 71 } 72 Ok(hex_encode(value.expose_secret()).into()) 73 } 74 75 pub(super) fn pbkdf2_sha256(password: &[u8], salt: &[u8], iterations: u32) -> SecretBox<[u8; 32]> { 76 SecretBox::init_with_mut(|output: &mut [u8; 32]| { 77 pbkdf2_hmac::<Sha256>(password, salt, iterations, output); 78 }) 79 } 80 81 fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { 82 if left.len() != right.len() { 83 return false; 84 } 85 left.iter() 86 .zip(right) 87 .fold(0_u8, |diff, (left, right)| diff | (left ^ right)) 88 == 0 89 } 90 91 fn random_bytes<const N: usize>() -> Result<[u8; N]> { 92 let mut bytes = [0_u8; N]; 93 getrandom(&mut bytes) 94 .map_err(|error| anyhow::anyhow!("secure random generation failed: {error}"))?; 95 Ok(bytes) 96 } 97 98 pub(super) fn hex_encode(bytes: impl AsRef<[u8]>) -> String { 99 const HEX: &[u8; 16] = b"0123456789abcdef"; 100 let mut encoded = String::with_capacity(bytes.as_ref().len() * 2); 101 for byte in bytes.as_ref() { 102 encoded.push(HEX[(byte >> 4) as usize] as char); 103 encoded.push(HEX[(byte & 0x0f) as usize] as char); 104 } 105 encoded 106 } 107 108 fn decode_hex(input: &str) -> Result<Vec<u8>> { 109 if input.len() % 2 != 0 { 110 bail!("hex string has odd length"); 111 } 112 let mut bytes = Vec::with_capacity(input.len() / 2); 113 for pair in input.as_bytes().chunks_exact(2) { 114 let high = decode_hex_nibble(pair[0])?; 115 let low = decode_hex_nibble(pair[1])?; 116 bytes.push((high << 4) | low); 117 } 118 Ok(bytes) 119 } 120 121 fn decode_hex_nibble(byte: u8) -> Result<u8> { 122 match byte { 123 b'0'..=b'9' => Ok(byte - b'0'), 124 b'a'..=b'f' => Ok(byte - b'a' + 10), 125 b'A'..=b'F' => Ok(byte - b'A' + 10), 126 _ => bail!("invalid hex character"), 127 } 128 } 129 130 #[cfg(test)] 131 mod tests { 132 use super::{hash_password, verify_password}; 133 134 #[test] 135 fn password_hash_rejects_unreasonable_kdf_iterations() { 136 let encoded = hash_password("password-123456").unwrap(); 137 let excessive = encoded.replacen("$210000$", "$1000000000$", 1); 138 139 let error = verify_password("password-123456", &excessive).unwrap_err(); 140 141 assert!( 142 error 143 .to_string() 144 .contains("unsupported password hash iteration count") 145 ); 146 } 147 }