Rust Password Hashing with Argon2id and the Sodiumoxide Crate

Written by Luke Arntz on
Filed under Code
Tagged as rust sodiumoxide password hashing libsodium

Article Contents


Summary

DISCLAIMER #1: I am not a cryptography expert. I’m not even good at rust. Please be diligent when handling other people’s private information. There is a lot more detail and authoritative information in the references section at the end of this post.

DISCLAIMER #2: If you have the option using of an external authentication provider that is probably better than building your own.

Crypto can be confusing, and there are very few examples of how to use the sodiumoxide crate. I needed to store some password hashes, and originally I was planning to use the ring crate and pbkdf2, but was encouraged to use sodiumoxide on the unofficial rust Discord channel.

I spent some time reviewing the documentation for sodiumoxide and while there were working and straight forward examples it wasn’t clear whether that was all that needed to be done to securely hash passwords, or if there were additional steps required when used in production.

I think I’ve found the answers to my initial questions and decided to write this post for my own reference and to help anyone else looking for help.

The one example in crate docs, does illustrate how this works, but does not provide much explanation.

Hopefully this post will add some clarity.

All code used in this post is available in this github repo sodiumoxide-password-hashing-examples.

Key Points

  1. The salt is provided by the hashing function and the hash, salt, and hashing parameters are stored as one piece of data.
  2. No additional salt is needed when using argon2id and the pwhash function.
  3. Depending on which database you are using to store the hashes in you may have to manipulate hashes before sending them to the DB.
  4. This process really is simple, but I wanted to provide some code that shows how to use it in an application.

Password Hashing

Password hashing is used for authentication. The basic premise is this, you store the hash of a user’s password in a database. When the user attempts to log in you hash the password value they give you and compare that to the previously stored hash of their password.

If the verification passes let them in.

Why Argon2id

Argon2id is resistent to two types of attacks. Side channel timing attacks and time-memory tradeoff attacks. If you’re interested in more detail have a look at this stackexchange question or the rfc.

Why sodiumoxide and libsodium

Mostly because libsodium is widely used and recommended by people that know more about cryptography than I. It provides a simple API – almost too simple considering the lack of examples and documentation.

What is sodiumoxide

The sodiumoxide crate is a wrapper for the c library libsodium that can be used within rust applications. Libsodium is wrapped by many languages other than rust, including C#, Python, and Java.

When it comes to rust, sodiumoxide is the repo with the most stars on github which must count for something, right? 🤷

Salts

Salts are included in the hash output from the pwhash function. The salt is generated randomly for every time the pwhash function is run. This means that if you call pwhash twice with the same password the function will create a new salt for both calls and thus the resulting hashes will be different.

This may be a bit different that what you may have read. In many cases you need to provide and store a salt yourself to allow for secure password verification.

When using libsodium/sodiumoxide an additional salt is not required.

Hashes

Sodiumoxide provides this as a [u8;128]. That is an array with 128 u8’s. It can easily be converted and stored as ascii text using the std::str::from_utf8() function. After conversion to a str you’ll be left with a 128 character string that is padded with null characters. These must be removed before storing in a postgres database with the trim_end_matches('\u{0}') function. Keep in mind that if you do this you need to add the null padding back before sending the hash to be verified.

Postgres can also store bytes in column of type bytea if you prefer to store the hash in binary form there is no trimming required. I will provide examples of these options below.

Examples

sodiumoxide::init()

The before you start hashing you should initialize the sodiumoxide library. Not doing this will only cause the hashing to take longer. Calling init() multiple times also does no harm.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use sodiumoxide::crypto::pwhash::argon2id13;
use std::time::Instant;


pub fn hash(passwd: &str) -> (String, argon2id13::HashedPassword) {
    sodiumoxide::init().unwrap();
    let hash = argon2id13::pwhash(
        passwd.as_bytes(),
        argon2id13::OPSLIMIT_INTERACTIVE,
        argon2id13::MEMLIMIT_INTERACTIVE,
    )
    .unwrap();
    let texthash = std::str::from_utf8(&hash.0).unwrap().to_string();
    (texthash, hash)
}

sodium_init() initializes the library and should be called before any other function provided by Sodium. It is safe to call this function more than once and from different threads – subsequent calls won’t have any effects.

See the libsodium docs for more details.

Choosing a hashing algorithm

Sodiumoxide provides the ability to hash using three different algorithms. They are:

  1. argon2i13 - Argon2 summarizes the state of the art in the design of memory-hard functions.
  2. argon2id13 - Argon2 summarizes the state of the art in the design of memory-hard functions.
  3. scryptsalsa208sha256 - a particular combination of Scrypt, Salsa20/8 and SHA-256

Internet consensus says that argon2id13 is the most secure.

If you’d like to use the recommended argon2id algorithm you’ll need to import it like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use sodiumoxide::crypto::pwhash::argon2id13;
use std::time::Instant;


pub fn hash(passwd: &str) -> (String, argon2id13::HashedPassword) {
    sodiumoxide::init().unwrap();
    let hash = argon2id13::pwhash(
        passwd.as_bytes(),
        argon2id13::OPSLIMIT_INTERACTIVE,
        argon2id13::MEMLIMIT_INTERACTIVE,
    )
    .unwrap();
    let texthash = std::str::from_utf8(&hash.0).unwrap().to_string();
    (texthash, hash)
}

Creating the hash

Creating the hash is done by the pwhash() function (from the algorithm’s module). This function will take the password as_bytes() and return a HashedPassword struct. In addition to the password we need to tell the pwhash function how much work to do when hashing the password. The more work done to hash the password the more time it would take for an attacker to crack it. But, that also means our application will have more work to do every time a password is verified so be careful and test what choose here.

The choices are given using provided constants.

The libsodium docs describe these options as listed:

For interactive, online operations, crypto_pwhash_OPSLIMIT_INTERACTIVE and crypto_pwhash_MEMLIMIT_INTERACTIVE provide base line for these two parameters. This currently requires 64 MiB of dedicated RAM. Higher values may improve security (see below).

Alternatively, crypto_pwhash_OPSLIMIT_MODERATE and crypto_pwhash_MEMLIMIT_MODERATE can be used. This requires 256 MiB of dedicated RAM, and takes about 0.7 seconds on a 2.8 Ghz Core i7 CPU.

For highly sensitive data and non-interactive operations, crypto_pwhash_OPSLIMIT_SENSITIVE and crypto_pwhash_MEMLIMIT_SENSITIVE can be used. With these parameters, deriving a key takes about 3.5 seconds on a 2.8 Ghz Core i7 CPU and requires 1024 MiB of dedicated RAM.

Note: these constants can be mixed and matched. The corresponding sodiumoxide constants are:

  • MEMLIMIT_INTERACTIVE and OPSLIMIT_INTERACTIVE
  • MEMLIMIT_MODERATE and OPSLIMIT_MODERATE
  • MEMLIMIT_SENSITIVE and OPSLIMIT_SENSITIVE

The Argon2id variant with t=1 and maximum available memory is RECOMMENDED as a default setting for all environments.

from argon rfc

However, that recommendation may not be a good fit for your use case. You will need to make sure your server can handle the load otherwise you will be more susceptible to a DoS. I’ve included code in the repo with crude timings of a few settings. It can be easily modified to test any combination of settings.

The algorithm that sodiumoxide uses is based on which module you import into your crate. Just importing sodiumoxide::crypto::pwhash will get you the scryptsalsa208sha256 algorithm.

Here’s a code example of hashing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use sodiumoxide::crypto::pwhash::argon2id13;
use std::time::Instant;


pub fn hash(passwd: &str) -> (String, argon2id13::HashedPassword) {
    sodiumoxide::init().unwrap();
    let hash = argon2id13::pwhash(
        passwd.as_bytes(),
        argon2id13::OPSLIMIT_INTERACTIVE,
        argon2id13::MEMLIMIT_INTERACTIVE,
    )
    .unwrap();
    let texthash = std::str::from_utf8(&hash.0).unwrap().to_string();
    (texthash, hash)
}

Verifying a password

Verifying passwords against hashes is almost as easy as creating the hashes. The verification is done by the function pwhash_verify.

1
2
3
4
5
6
7
pub fn verify(hash: [u8; 128], passwd: &str) -> bool {
    sodiumoxide::init().unwrap();
    match argon2id13::HashedPassword::from_slice(&hash) {
        Some(hp) => argon2id13::pwhash_verify(&hp, passwd.as_bytes()),
        _ => false,
    }
}

This function takes two arguments. First is the HashedPassword struct containing the stored password hash. The second is the given password that needs to be verified. This must be provided as_bytes().

A new HashedPassword struct can be created from the stored password hash using the HashedPassword::from_slice() function.

IMPORTANT: There is a gotcha here. The slice or array provided to the HashedPassword::from_slice() function MUST have a size of 128! If it does not the verification will fail even with a correct password.

The reason I mention this will become more clear in the next section when we discuss storing the hash in a database. If the hash is transformed before storage it must be transformed back to the correct size before creating a new HashedPassword.

This means padding the array/slice to size 128 with the NULL character.

Storing Hashes

As mentioned, we need to transform the hash to something a database will be happy with.

There are two options here. The first is storing the hash as binary data. The second is to convert the hash to ascii text and store that.

Storing as ASCII

Libsodium provided output from crypto_pwhash_str is ascii encoded

The output string is zero-terminated, includes only ASCII characters and can be safely stored into SQL databases and other data stores. No extra information has to be stored in order to verify the password.

Sodiumoxide doesn’t return a string, but does give us a u8 array that can be used to create an str using the std::str::from_utf8() function. However, the resulting str will be padded with NULL characters ('\u{0}') so an additional call to trim_end_matches('\u{0}') is needed to remove them.

The reason we want to remove them is that postgresql will totally flip out if you try to put a string containing NULLs in a text data type column.

According to the mailing list it is because:

You’re trying to insert a string which contains a ‘\0’ character. The server can’t handle strings containing embedded NULs, as it uses C-style string termination internally.

So this brings us back to having to pad our hash with nulls before sending it for verification using argon2id13::pwhash_verify().

This process is pretty straight forward. First, create a zeroed array sized at 128. Then iterate through the hash chars and copy them into the array.

1
2
3
4
5
6
7
8
9
let user = database::get_user(String::from(args.get(1).unwrap())).await?;
let mut padded = [0u8; 128];
user.password_hash_char
    .as_bytes()
    .iter()
    .enumerate()
    .for_each(|(i, val)| {
        padded[i] = val.clone();
    });

The array padded can then be used with HashedPassword::from_slice() to create a new HashedPassword struct that will verify correctly if the given password matches.

Storing as binary

Postgres also has a binary column type called bytea that can be used to store the binary hash without any transformation.

To store the hash as binary we simply give pass the array to sqlx as a slice.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
pub async fn add_user(user: UserDBRecord) -> Result<u64, sqlx::Error> {
    let conn =
        PgConnection::connect(&env::var("DATABASE_URL").expect("can't get no env::var")).await?;


    let result = sqlx::query!(
        r#"INSERT INTO users(user_name,password_hash_bin,password_hash_char,email_address)
            VALUES ($1,$2,$3,$4)
            ON CONFLICT(user_name) DO UPDATE SET password_hash_bin = $2
        "#,
        user.user_name,
        &user.password_hash_bin.0[..],
        user.password_hash_char,
        user.email_address
    )
    .execute(conn)
    .await;


    result
}

And when we want to verify a password we can directly use the slice returned from the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub async fn get_user(user_name: String) -> Result<UserDBRecordWithId, sqlx::Error> {
    let mut conn =
        PgConnection::connect(&env::var("DATABASE_URL").expect("can't get no env::var")).await?;


    let user_select = sqlx::query!(
        r#"
        SELECT id,user_name,password_hash_bin,password_hash_char,email_address
        FROM users WHERE user_name = $1"#,
        user_name
    )
    .fetch_one(&mut conn)
    .await?;

    Ok(UserDBRecordWithId {
        id: user_select.id,
        user_name: user_select.user_name,
        password_hash_bin: HashedPassword::from_slice(&user_select.password_hash_bin).unwrap(),
        password_hash_char: user_select.password_hash_char,
        email_address: user_select.email_address,
    })
}

Bytea vs Text

So which option is best? According to this post on the Pivotal blog (2016) using a text column type gives ~15% better read performance and equivalent write performance.

I haven’t done my own tests and theirs were in 2016, but even given the extra work our application has to do to convert and pad data it seems storing hashes as text is the better option.

Conclusion

All the code in this post is available in a github repo. Full examples of hashing and verifying passwords is provided. There are also examples of storing hashes as both text and binary data along with the code needed to transform data when required.

If you have suggestions to optimize the code in the repos I’d love to know. Please create an issue or pull request so we can all learn!

I am hesitant to make a suggestion here, but I’m going to anyway.

When hashing passwords with rust I would:

  1. Use the sodiumoxide crate.
  2. Use the argon2id13 module (argon2id algorithm).
  3. Use the OPSLIMIT_INTERACTIVE and MEMLIMIT_INTERACTIVE settings unless your application has an abundance of CPU power and/or low traffic, or the hashing process is not time constrained (e.g, offline tasks).
  4. When using postgresql store hashed passwords as ASCII text.

I hope this post at least provides some useful examples for anyone new to password hashing. I really enjoy learning rust and hope to provide other beginners with helpful information.

Thanks for reading!

References

Related Articles

Top