Skip to content
Blog ยป Password Hashing with PBKDF2 in Rust using Ring

Password Hashing with PBKDF2 in Rust using Ring

    The Ring cryptography library provides an implementation of PBKDF2, a standardised key derivation function that can be used to securely hash passwords. Using the ring::pbkdf2 module, we can derive a password hash and then also verify whether or not a hash matches a given password.

    Introduction to PBKDF2

    Password Based Key Derivation Function 2 (PBKDF2) is specified in RFC 2898 Section 5.2. PBKDF2 is not a very modern algorithm but it is still considered secure as long as it is used correctly with carefully chosen parameters. PBKDF2 applies a pseudorandom function (such as a HMAC) to the input password and salt value and repeats the process a number of times to produce a derived key which can then be used as a cryptographic key in subsequent operations. The added computational work of the numerous iterations makes password cracking very difficult. PBKDF2 is also recommended by NIST and has implementations conforming to FIPS-140.

    The RFC document defines the the PBKDF2 function as follows:

    PBKDF2 (P, S, c, dkLen) -> DK

    These are the PBKDF2 function parameters:

    Options:        PRF        underlying pseudorandom function (hLen
                               denotes the length in octets of the
                               pseudorandom function output)
    
    Input:          P          password, an octet string
                    S          salt, an octet string
                    c          iteration count, a positive integer
                    dkLen      intended length in octets of the derived
                               key, a positive integer, at most
                               (2^32 - 1) * hLen
    
    Output:         DK         derived key, a dkLen-octet string

    PBKDF2 Usage Guidelines

    • Weak passwords can be vulnerable to brute force attacks so users should be required to set strong passwords that are long and contain sufficient entropy.
    • Using a low entropy salt can make the hash (derived key) vulnerable to pre-computation attacks using rainbow tables. The salt parameter improves the security of PBKDF2 by changing the output of the hash for any given input password. The salt should be unpredictable and be selected to be unique per user and per database (this part is sometimes called a pepper).
    • Using a low number of iterations can also make the hash vulnerable to brute force attacks. The larger the number of iterations selected, the longer it takes to run the PBKDF2 function and therefore requires more work to successfully crack passwords. The number of iterations which you should select is a moving target and increases over time as cpu power increases. OWASP provides a good resource on the recommend parameters here. Currently for PBKDF2-HMAC-SHA256 the recommend iteration count is 600000 and for PBKDF2-HMAC-SHA512 it is 210000.
    • The output key length needs to be long enough to prevent brute force attacks and so a minimum key length of at least 128 bits is recommend. Generally the output length should be set to be equal to the length of the chosen hash function.
    • When PBKDF2 is used with a HMAC, manual pre-hashing of the password may be required in some cases to prevent certain denial of service vulnerabilities. This can occur when the input password is longer than the block size of the hash function. See here for more details.

    Summary of Types

    The ring::pbkdf2 module contains the following types and functions:

    struct Algorithm โ€“ The type of PBKDF2 algorithm. PBKDF2_HMAC_SHA1, PBKDF2_HMAC_SHA256PBKDF2_HMAC_SHA384 and PBKDF2_HMAC_SHA512 algorithms are supported.

    fn derive – The PBKDF2 algorithm used to derive a password hash from a given password. We pass in the chosen algorithm, the number of iterations to use, a salt and the secret (password). The hash value is stored in the given byte array, out. The size of the out array determines the size of the hash value returned.

    pub fn derive(
        algorithm: Algorithm, 
        iterations: NonZeroU32, 
        salt: &[u8], 
        secret: &[u8], 
        out: &mut [u8]
    )

    fn verify – Checks if a given secret (password) matches a previously derived hash. We pass in the algorithm, the number of iterations, the salt, the secret to check and the hash which was previously derived using the pbkdf2::derive function.

    pub fn verify(
        algorithm: Algorithm, 
        iterations: NonZeroU32, 
        salt: &[u8], 
        secret: &[u8], 
        previously_derived: &[u8]
    ) -> Result<(), Unspecified>

    Rust Imports

    Let’s start by importing the required types into our project.

    use std::num::NonZeroU32;
    use ring::digest::SHA256_OUTPUT_LEN;
    use ring::digest::SHA512_OUTPUT_LEN;
    use ring::pbkdf2;
    use ring::pbkdf2::Algorithm;
    use ring::pbkdf2::PBKDF2_HMAC_SHA1;
    use ring::pbkdf2::PBKDF2_HMAC_SHA256;
    use ring::pbkdf2::PBKDF2_HMAC_SHA384;
    use ring::pbkdf2::PBKDF2_HMAC_SHA512;

    Prepare Iterations, Salt & Secret

    In the code below we declare the parameters to be passed into the pbkdf2::derive function. In this example we are using the PBKDF2_HMAC_SHA256 algorithm and so iterations is set to 600000 as recommend by OWASP. The iterations variable is a NonZeroU32 type from the Rust standard library which prevents setting an iteration count of zero.

    const PBKDF2_HMAC_SHA256_ITERATIONS: u32 = 600_000; // number recommended by OWASP for PBKDF2 with SHA256
    
    let iterations = NonZeroU32::new(PBKDF2_HMAC_SHA256_ITERATIONS).unwrap();
    let salt = b"random salt"; // this should be randomly generated, using some user specific component and database specific component
    let secret = b"strong password"; // select a strong password
    println!("Secret/password value: {}", hex::encode(secret)); // don't print this in production

    Derive the Password Hash & Store

    Next we call the pbkdf2::derive function with our chosen parameters and store the hash in a byte array. In this example the size of the output hash will be the same length as the output of SHA256 but we could make this longer if required. The length is set by choosing the size of the password_hash array.

    let mut password_hash = [0u8; SHA256_OUTPUT_LEN]; // initialise with zeros
    pbkdf2::derive(PBKDF2_HMAC_SHA256, iterations, salt, secret, &mut password_hash);
    println!("Password hash: {}", hex::encode(password_hash)); // don't print this in production

    Verify a Password Matches the Password Hash

    Using the same input parameters as above we can call the pbkdf2::verify function to check if a given password matches the previously derived hash. If the password matches the hash then the verify function returns a Result::Ok type containing a (), otherwise it returns a Result::Err containing error::Unspecified (defined in the Ring error module).

    pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, secret, &password_hash).unwrap(); // success case
    pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, "wrong password".as_bytes(), &password_hash).unwrap(); // failure case

    Full Sample Code

    Here is the full code sample for reference. The scenario 2 example below uses PBKDF2_HMAC_SHA512 with the recommended iteration count of 210000 and uses an output hash size equal to the size of SHA512.

    use std::num::NonZeroU32;
    use ring::digest::SHA256_OUTPUT_LEN;
    use ring::digest::SHA512_OUTPUT_LEN;
    use ring::pbkdf2;
    use ring::pbkdf2::Algorithm;
    use ring::pbkdf2::PBKDF2_HMAC_SHA1;
    use ring::pbkdf2::PBKDF2_HMAC_SHA256;
    use ring::pbkdf2::PBKDF2_HMAC_SHA384;
    use ring::pbkdf2::PBKDF2_HMAC_SHA512;
    
    fn main() {
        // scenario 1 - PBKDF2_HMAC_SHA256
        const PBKDF2_HMAC_SHA256_ITERATIONS: u32 = 600_000; // number recommended by OWASP for PBKDF2 with SHA256
    
        // Prepare iterations, salt and secret
        let iterations = NonZeroU32::new(PBKDF2_HMAC_SHA256_ITERATIONS).unwrap();
        let salt = b"random salt"; // this should be randomly generated, using some user specific component and database specific component
        let secret = b"strong password"; // select a strong password
        println!("Secret/password value: {}", hex::encode(secret)); // don't print this in production
    
        // Derive the password hash and store
        let mut password_hash = [0u8; SHA256_OUTPUT_LEN]; // initialise with zeros
        pbkdf2::derive(PBKDF2_HMAC_SHA256, iterations, salt, secret, &mut password_hash);
        println!("Password hash: {}", hex::encode(password_hash)); // don't print this in production
    
        // Verify whether or not a password matches the stored password hash
        pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, secret, &password_hash).unwrap(); // success case
        //pbkdf2::verify(PBKDF2_HMAC_SHA256, iterations, salt, "wrong password".as_bytes(), &password_hash).unwrap(); // failure case
    
    
        // scenario 2 - PBKDF2_HMAC_SHA512
        const PBKDF2_HMAC_SHA512_ITERATIONS: u32 = 210_000; // number recommended by OWASP for PBKDF2 with SHA512
    
        // Prepare iterations, salt and secret
        let iterations = NonZeroU32::new(PBKDF2_HMAC_SHA512_ITERATIONS).unwrap();
        let salt = b"random salt"; // this should be randomly generated, using some user specific component and database specific component
        let secret = b"strong password"; // select a strong password
        println!("Secret/password value: {}", hex::encode(secret)); // don't print this in production
    
        // Derive the password hash and store
        let mut password_hash = [0u8; SHA512_OUTPUT_LEN]; // initialise with zeros
        pbkdf2::derive(PBKDF2_HMAC_SHA512, iterations, salt, secret, &mut password_hash);
        println!("Password hash: {}", hex::encode(password_hash)); // don't print this in production
    
        // Verify whether or not a password matches the stored password hash
        pbkdf2::verify(PBKDF2_HMAC_SHA512, iterations, salt, secret, &password_hash).unwrap(); // success case
        //pbkdf2::verify(PBKDF2_HMAC_SHA512, iterations, salt, "wrong password".as_bytes(), &password_hash).unwrap(); // failure case
    }

    Conclusion

    In this post we introduced the PBKDF2 algorithm, explained how it works and described how to correctly choose the parameters for the PBKDF2 function. We then explained how to use the Ring pbkdf2 module to derive a password hash by using the pbkdf2::derive function and verify a password matches a password hash using the pbkdf2::verify function. The examples show two scenarios using the most commonly used PBKDF2 algorithms; PBKDF2_HMAC_SHA256 and PBKDF2_HMAC_SHA512.

    Leave a Reply

    Your email address will not be published. Required fields are marked *