1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#![cfg(feature = "include_simple")]
use alloc::{string::String, vec};
use core::convert::TryInto;

use crate::errors::CheckError;
use hmac::Hmac;
use rand_core::RngCore;
use sha2::Sha256;
use subtle::ConstantTimeEq;

use super::pbkdf2;

#[cfg(not(features = "thread_rng"))]
type DefaultRng = rand_core::OsRng;
#[cfg(features = "thread_rng")]
type DefaultRng = rand::ThreadRng;

/// A helper function that should be sufficient for the majority of cases where
/// an application needs to use PBKDF2 to hash a password for storage.
///
/// Internally it uses PBKDF2-HMAC-SHA256 algorithm. The result is a `String`
/// that contains the parameters used as part of its encoding. The `pbkdf2_check`
/// function may be used on a password to check if it is equal to a hashed value.
///
/// # Format
///
/// The format of the output is a modified version of the Modular Crypt Format
/// that encodes algorithm used and iteration count. The format is indicated as
/// "rpbkdf2" which is short for "Rust PBKF2 format."
///
/// ```text
/// $rpbkdf2$0$<base64(c)>$<base64(salt)>$<based64(hash)>$
/// ```
///
/// # Arguments
///
/// * `password` - The password to process
/// * `c` - The iteration count
pub fn pbkdf2_simple(password: &str, rounds: u32) -> Result<String, rand_core::Error> {
    // 128-bit salt
    let mut salt = [0u8; 16];
    DefaultRng::default().try_fill_bytes(&mut salt)?;

    // 256-bit derived key
    let mut dk = [0u8; 32];

    pbkdf2::<Hmac<Sha256>>(password.as_bytes(), &salt, rounds, &mut dk);

    let mut result = String::with_capacity(90);
    result.push_str("$rpbkdf2$0$");
    result.push_str(&base64::encode(&rounds.to_be_bytes()));
    result.push('$');
    result.push_str(&base64::encode(&salt));
    result.push('$');
    result.push_str(&base64::encode(&dk));
    result.push('$');

    Ok(result)
}

/// Compares a password against the result of a `pbkdf2_simple`.
///
/// It will return `Ok(())` if `password` hashes to the same value, if hashes
/// are different it will return `Err(CheckError::HashMismatch)`, and
/// `Err(CheckError::InvalidFormat)` if `hashed_value` has an invalid format.
///
/// # Arguments
/// * `password` - The password to process
/// * `hashed_value` - A string representing a hashed password returned by
/// `pbkdf2_simple`
pub fn pbkdf2_check(password: &str, hashed_value: &str) -> Result<(), CheckError> {
    let mut parts = hashed_value.split('$');
    // prevent dynamic allocations by using a fixed-size buffer
    let buf = [
        parts.next(),
        parts.next(),
        parts.next(),
        parts.next(),
        parts.next(),
        parts.next(),
        parts.next(),
        parts.next(),
    ];

    // check the format of the input: there may be no tokens before the first
    // and after the last `$`, tokens must have correct information and length.
    let (count, salt, hash) = match buf {
        [Some(""), Some("rpbkdf2"), Some("0"), Some(c), Some(s), Some(h), Some(""), None] => {
            (c, s, h)
        }
        _ => return Err(CheckError::InvalidFormat),
    };

    let count_arr = base64::decode(count)?
        .as_slice()
        .try_into()
        .map_err(|_| CheckError::InvalidFormat)?;
    let count = u32::from_be_bytes(count_arr);
    let salt = base64::decode(salt)?;
    let hash = base64::decode(hash)?;

    let mut output = vec![0u8; hash.len()];
    pbkdf2::<Hmac<Sha256>>(password.as_bytes(), &salt, count, &mut output);

    // Be careful here - its important that the comparison be done using a fixed
    // time equality check. Otherwise an adversary that can measure how long
    // this step takes can learn about the hashed value which would allow them
    // to mount an offline brute force attack against the hashed password.
    if output.ct_eq(&hash).unwrap_u8() == 1 {
        Ok(())
    } else {
        Err(CheckError::HashMismatch)
    }
}