[ rust ] [ sodiumoxide ] [ password ] [ hashing ] [ libsodium ]
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
- The salt is provided by the hashing function and the hash, salt, and hashing parameters are stored as one piece of data.
- No additional salt is needed when using argon2id and the
pwhash
function. - Depending on which database you are using to store the hashes in you may have to manipulate hashes before sending them to the DB.
- 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.
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:
- argon2i13 - Argon2 summarizes the state of the art in the design of memory-hard functions.
- argon2id13 - Argon2 summarizes the state of the art in the design of memory-hard functions.
- 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:
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:
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
.
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.
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.
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.
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:
- Use the sodiumoxide crate.
- Use the argon2id13 module (argon2id algorithm).
- 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).
- 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
- libsodium: the pwhash API
- sodiumoxide github repo
- sodiumoxide docs.rs
- stackoverflow: is argon2 safe only using the password
- stackoverflow: why do to Argon hashes with the same password differ?
- stackexchange: why use argon2id
- ietf: Argon2 Draft
- NaCl website
- pivotal bytea vs text comparison
- simple crypt using sodiumoxide