Functional Weave
Code in Rust

auth.password-hash@1.0.0

impl/rust/verify_password.rs

3,088 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::crypto_constant_time_equal::constant_time_equal;  ← from crypto.constant-time-equal ^1.0.0 · built alongside by fune
use super::crypto_pbkdf2_sha256::pbkdf2_sha256;  ← from crypto.pbkdf2-sha256 ^1.0.0 · built alongside by fune
use super::encoding_base64_base64_decode::base64_decode;

const MALFORMED: &str = "stored password hash is malformed";

fn sextet(b: u8) -> i64 {
    match b {
        b'A'..=b'Z' => (b - b'A') as i64,
        b'a'..=b'z' => (b - b'a' + 26) as i64,
        b'0'..=b'9' => (b - b'0' + 52) as i64,
        b'+' => 62,
        b'/' => 63,
        _ => -1,
    }
}

/// Would base64_decode accept this, and give at least one byte? Checked
/// first so a bad field is reported as a malformed hash, with the words the
/// other languages use, rather than as base64's own panic.
fn is_base64(text: &str) -> bool {
    let b = text.as_bytes();
    if b.is_empty() || b.len() % 4 != 0 {
        return false;
    }
    let padding = b.iter().rev().take_while(|&&c| c == b'=').count();
    if padding > 2 || b.len() - padding == 0 {
        return false;
    }
    let body = &b[..b.len() - padding];
    if body.iter().any(|&c| sextet(c) < 0) {
        return false;
    }
    let last = sextet(body[body.len() - 1]);
    match padding {
        1 => last & 3 == 0,
        2 => last & 15 == 0,
        _ => true,
    }
}

/// (iterations, salt, hash) of a stored pbkdf2_sha256 string. A string this
/// code did not write is a data problem to surface, not a wrong password.
///
/// # Panics
/// Panics when the string is not in that form.
pub fn parse_stored_hash(stored: &str) -> (i64, Vec<i64>, Vec<i64>) {
    let parts: Vec<&str> = stored.split('$').collect();
    if parts[0] != "pbkdf2_sha256" {
        panic!("stored password hash is not a pbkdf2_sha256 hash");
    }
    if parts.len() != 4 {
        panic!("{}", MALFORMED);
    }
    let count = parts[1].as_bytes();
    if count.is_empty() || count.len() > 10 || count[0] == b'0' || !count.iter().all(|c| c.is_ascii_digit()) {
        panic!("{}", MALFORMED);
    }
    let iterations = count.iter().fold(0i64, |n, c| n * 10 + i64::from(c - b'0'));
    if !is_base64(parts[2]) || !is_base64(parts[3]) {
        panic!("{}", MALFORMED);
    }
    (iterations, base64_decode(parts[2]), base64_decode(parts[3]))
}

/// Does the password match the stored hash? Re-derived with the stored salt
/// and iteration count, compared in constant time.
///
/// # Panics
/// Panics when the stored string is not one hash_password makes.
pub fn verify_password(password: &str, stored: &str) -> bool {
    let (iterations, salt, expected) = parse_stored_hash(stored);
    let bytes: Vec<i64> = password.bytes().map(|b| b as i64).collect();
    constant_time_equal(&pbkdf2_sha256(&bytes, &salt, iterations, expected.len() as i64), &expected)
}

pub fn fune_vector(args: &[Value]) -> Value {
    let password = match &args[0] {
        Value::Str(s) => s.as_str(),
        _ => panic!("password must be a string"),
    };
    let stored = match &args[1] {
        Value::Str(s) => s.as_str(),
        _ => panic!("stored password hash must be a string"),
    };
    Value::Bool(verify_password(password, stored))
}