iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }