Which crate should I use for Argon2?

I'm writing a password manager that will be using the Argon2 function. At the moment there are 3 crates that provide it: argon2rs, rust-argon2 and argonautica.

argon2rs is by far the most popular, is written in pure Rust and uses SIMD, but hasn't been updated in 3 years and does not implement Argon2id variant.

argonautica is the most idiomatic and well documented. It was updated recently, implements Argon2id, uses SIMD and is generally speaking pretty awesome, but wraps around the reference implementation in C, rather than provide a pure Rust implementation. Author claims to be planning to include a Rust implementation at some point, but considering the fairly slow development I'm unsure if that will ever be the case.

rust-argon2 is written in pure Rust, implements Argon2id and was recently updated, but it claims to be unoptimized and does not use SIMD.

I'm leaning towards argonautica since I will likely need to use non-pure Rust for GUI anyway, but I would like to hear opinions of more experienced programmers.

4 Likes

For password hashing function, I'd choose simply one that has the fastest implementation. Anything less than fastest gives your adversary an advantage.

Quite literally going through the exact same decision and conclusions right now. argonautica looked like the best approach, but it's object oriented design is really going against the grain as I aim for a more functional one. I think a functional approach would be nicer here.

Would you be so kind to explain to those of us with less understanding of the intricacies of crypto why that is? Thanks in advance.

1 Like

Hashed passwords can be broken with dictionary attacks, i.e. by hashing billions of different passwords, and looking for a matching hash. The way to stop such attacks is to make the hashing algorithm so slow that brute-forcing becomes impractical, i.e. it'd take too long time or cost too much to compute so many hashes.

So you should pick and configure the hashing algorithm to be as slow as you can tolerate. But if your implementation is inefficient, you'll be burning your CPU on the inefficiency, but not on the essence of the algorithm. Someone else could pick a more efficient implementation for cracking the hash, and won't be slowed by your inefficiency.

Let's say you accept a 100ms delay for log in, and burn 100ms of your CPU time on the hashing. If the attacker can find an implementation that is 50% faster, then they can do the same in just 50ms. If you've used the faster implementation you could either also have a faster login, or you could double the hash strength (so your attacker would have to spend the same 100ms of CPU time as you).

8 Likes

Considering @kornel's advice, I decided to benchmark these crates. This isn't entirely straightforward:

  • The Argon2 algorithm lets you adjust the number of iterations, the amount of memory used, and the number of threads to busy, to make sure password hashing takes long enough to make attacks as impractical as you need. Obviously, you need to run all three crates with equivalent parameters to make a useful comparison.
  • Some crates have features to enable the use of SIMD.

I think I managed to get things lined up. Here are my results:

running 3 tests
test bench_argon2rs    ... bench: 216,181,118 ns/iter (+/- 6,126,616)
test bench_argonautica ... bench: 189,841,017 ns/iter (+/- 2,232,554)
test bench_rust_argon2 ... bench: 520,689,832 ns/iter (+/- 4,013,588)

I enabled the "simd" feature for Argonautica. I tried to enable the "simd" feature on argon2rs, but it didn't compile. rust_argon2 has no SIMD feature.

Here's my Cargo.toml:

[package]
name = "play-argon"
version = "0.1.0"
authors = ["Jim Blandy <jimb@...>"]
edition = "2018"

[dependencies]
argon2rs = "0.2.5" # SIMD doesn't build (unrecognized `extern "platform-intrinsic"` function)
argonautica = { version = "0.2", features = ["simd"] }
rust-argon2 = "0.8.2"

Here's my code:

#![feature(test)]

extern crate test;
use test::Bencher;

const PASSWORD: &str = "correct horse battery staple";
const SALT: &str = "Along Russian's 'Road of Bones,' Relics of Suffering and Despair";

#[bench]
fn bench_argon2rs(b: &mut Bencher) {
    use argon2rs::{Argon2, Variant};

    let argon2 = Argon2::new(192, 8, 4096, Variant::Argon2d).unwrap();
    let mut out = [0_u8; 32];
    b.iter(|| argon2.hash(&mut out, PASSWORD.as_bytes(), SALT.as_bytes(), &[], &[]))
}

#[bench]
fn bench_argonautica(b: &mut Bencher) {
    use argonautica::Hasher;
    use argonautica::config::Variant;
    b.iter(|| {
        Hasher::default()
            .configure_iterations(192)
            .configure_lanes(8)
            .configure_memory_size(4096)
            .configure_variant(Variant::Argon2d)
            .opt_out_of_secret_key(true)
            .with_password(PASSWORD)
            .with_salt(SALT)
            .hash()
            .unwrap()
    })
}

#[bench]
fn bench_rust_argon2(b: &mut Bencher) {
    use argon2::{self, Config, Variant};

    let config = Config {
        time_cost: 192,
        lanes: 8,
        mem_cost: 4096,
        variant: Variant::Argon2d,
        .. Config::default()
    };
    b.iter(|| {
        argon2::hash_encoded(PASSWORD.as_bytes(), SALT.as_bytes(), &config).unwrap()
    });
}

fn main() {}

If there's something I can improve to make this a better comparison, please let me know.

1 Like

Hello! I signed up an account on the Rust Language community exclusively to reply to this thread. (EDIT: Missed the April 2019 comment above, but keeping below as a record of the conversation).

@kornel :

For password hashing function, I’d choose simply one that has the fastest implementation.

That's true for regular hashing, not password hashing. Anyone who uses Argon2 should be using it because it's slow. There are plenty of fast algorithms that offer as much security as Argon2, such as SHA-3 and BLAKE3. Slow algorithms like Argon2 are designed to reduce the effectiveness of bruteforce attacks.

@kornel :

Anything less than fastest gives your adversary an advantage.

The opposite is true. The faster the algorithm is, the greater advantage an adversary has. This is a very logical concept, your password is at greater risk of being bruteforced if the algorithm can produce a billion hashes per second instead of ten thousand.

Of course, the algorithm is slow. But why should one choose the slow implementation, if there is faster one with the same semantics?

1 Like

If the post I quoted didn't refer to an adversarial advantage, then I'd agree I misunderstood what they meant. The performance of your chosen implementation is not relevant to an adversary, unless they are attempting a bruteforce attack through your application (in which case the assertion I made is still valid, more performant=more beneficial for an attacker).

If I understood correctly, the argument was that by selecting the more efficient implementation one can change parameters for the algorithm to make bruteforce attack more time-consuming, without sacrificing the efficiency of own system.

3 Likes

While this is true, you completely misunderstood @kornel 's argument. His point was that by selecting a slower library for the same algorithm, you can't afford as many rounds of hashing as you could if you had used a faster one.

I.e., because the total hashing time is determined by the intrinsic slowness of the algorithm and by the execution time of the concrete code together, if you blow your time budget on a slower implementation, you won't be able to spend time on more rounds. This will in turn mean that the attacker will also have to perform fewer rounds, giving him/her a higher chance of a successful brute-force attack.

(Or, if you choose to go with as many rounds as you would if you had used a faster library anyway, then of course this security disadvantage goes away, however, your system might look unacceptably slow to more people, which is not a security problem, it's "only" annoying.)

4 Likes

Now we're all on the same page. I had only seen the March 2019 post and missed the April 2019 post clarifying the point about the inefficiency of multiple rounds as the cost to the defender.

I also edited my first comment, so nobody else gets confused. Password hashing and fast aren't usually in the same sentence. I think I would have understood it better had it been phrased as "most efficient" rather than "fastest".

Elaborating a bit more, IMO fast suggests a property of the algorithm, whereas efficient suggests a property of the implementation.

4 Likes

This topic was automatically closed 30 days after the last reply. We invite you to open a new topic if you have further questions or comments.