iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }