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_SHA256
, PBKDF2_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
.