darkbio-crypto 0.19.3

Cryptography primitives and wrappers
Documentation
// crypto-rs: cryptography primitives and wrappers
// Copyright 2025 Dark Bio AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//! Argon2id cryptography wrappers and parametrization.
//!
//! <https://datatracker.ietf.org/doc/html/rfc9106>
//!
//! Turns a password and a salt into key material, made deliberately slow and
//! memory hungry so guessing passwords is expensive. The output is wiped when
//! dropped. For stretching a secret that is already random, see the `hkdf`
//! module instead.

use argon2::{Algorithm, Argon2, Block, Params, Version};
use zeroize::{Zeroize, Zeroizing};

/// key derives a key from the password, salt, and cost parameters using
/// Argon2id returning a fixed-size byte array that can be used as a
/// cryptographic key.
///
/// For example, a 32 byte key using RFC 9106's recommended profile for
/// memory-constrained environments:
///
/// ```
/// use darkbio_crypto::argon2;
///
/// // Example salt only; generate and store a fresh random 16-byte salt in real code.
/// let salt = b"example salt1234";
/// let key = argon2::key::<32>(b"password", salt, 3, 64 * 1024, 4);
/// assert_eq!(key.len(), 32);
/// ```
///
/// [RFC 9106 Section 4] recommends `time = 1`, `memory = 2 * 1024 * 1024`
/// (2 GiB), and `threads = 4`. Its second recommendation uses `time = 3`,
/// `memory = 64 * 1024` (64 MiB), and `threads = 4`. Both use a random 16-byte
/// salt and a 32-byte output.
///
/// `time` is the number of passes and `memory` is the total working memory in
/// KiB. `threads` is Argon2's lane count, an algorithm parameter that changes
/// the derived key. This crate configures its Argon2 dependency for serial
/// execution; this argument does not select an OS thread count. Store the salt
/// and all cost parameters so the same key can be reproduced on other devices.
///
/// # Panics
///
/// Panics if any of these input limits are violated:
///
/// - `time` must be at least 1.
/// - `threads` must be in `1..=0x00ff_ffff`.
/// - `memory` must be at least `8 * threads` KiB.
/// - `salt` must contain 8 to `u32::MAX` bytes.
/// - `password` must contain at most `u32::MAX` bytes.
/// - `N` must be in `4..=u32::MAX`.
///
/// Also panics if allocation of the working memory fails.
///
/// [RFC 9106 Section 4]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
pub fn key<const N: usize>(
    password: &[u8],
    salt: &[u8],
    time: u32,
    memory: u32,
    threads: u32,
) -> Zeroizing<[u8; N]> {
    let params = Params::new(memory, time, threads, Some(N)).unwrap();
    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);

    let mut output = Zeroizing::new([0u8; N]);
    hash(&argon2, password, salt, output.as_mut());
    output
}

/// key_with_len derives a key from the password, salt, and cost parameters
/// using Argon2id returning a byte vector that can be used as a cryptographic
/// key. See [`key`] for parameter meanings and recommended profiles.
///
/// This method is analogous to [`key`] but takes the output size as a parameter
/// and returns a vector instead of a fixed-sized array. Its purpose is to be
/// used on FFI interfaces where generics break composability. Do not use it in
/// Rust code.
///
/// # Panics
///
/// Panics for the same invalid inputs as [`key`], with `out` in place of `N`,
/// or if allocation of the working memory fails.
pub fn key_with_len(
    password: &[u8],
    salt: &[u8],
    time: u32,
    memory: u32,
    threads: u32,
    out: usize,
) -> Zeroizing<Vec<u8>> {
    let params = Params::new(memory, time, threads, Some(out)).unwrap();
    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);

    let mut output = Zeroizing::new(vec![0u8; out]);
    hash(&argon2, password, salt, &mut output);
    output
}

/// hash runs Argon2id over the password and salt into the output buffer using
/// working memory owned by this crate, which gets wiped before returning.
fn hash(argon2: &Argon2, password: &[u8], salt: &[u8], output: &mut [u8]) {
    // The allocating Argon2 API never wipes its working memory, so own it here.
    // Reserving first keeps an allocation failure a panic instead of an abort.
    let count = argon2.params().block_count();
    let mut blocks = Vec::new();
    blocks.try_reserve_exact(count).unwrap();
    blocks.resize(count, Block::new());

    let result =
        argon2.hash_password_into_with_memory(password, salt, output, blocks.as_mut_slice());

    // Wipe through the word slices, the Block impl goes word by word instead
    for block in blocks.iter_mut() {
        block.as_mut().zeroize();
    }
    result.unwrap();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_argon2id_vectors() {
        // Test vectors from Go's x/crypto/argon2 package.
        // Copyright 2017 The Go Authors. All rights reserved.
        // https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.39.0:argon2/argon2_test.go
        struct TestCase {
            time: u32,
            memory: u32,
            threads: u32,
            hash: &'static str,
        }
        let tests = [
            TestCase {
                time: 1,
                memory: 64,
                threads: 1,
                hash: "655ad15eac652dc59f7170a7332bf49b8469be1fdb9c28bb",
            },
            TestCase {
                time: 2,
                memory: 64,
                threads: 1,
                hash: "068d62b26455936aa6ebe60060b0a65870dbfa3ddf8d41f7",
            },
            TestCase {
                time: 2,
                memory: 64,
                threads: 2,
                hash: "350ac37222f436ccb5c0972f1ebd3bf6b958bf2071841362",
            },
            TestCase {
                time: 3,
                memory: 256,
                threads: 2,
                hash: "4668d30ac4187e6878eedeacf0fd83c5a0a30db2cc16ef0b",
            },
            TestCase {
                time: 4,
                memory: 4096,
                threads: 4,
                hash: "145db9733a9f4ee43edf33c509be96b934d505a4efb33c5a",
            },
            TestCase {
                time: 4,
                memory: 1024,
                threads: 8,
                hash: "8dafa8e004f8ea96bf7c0f93eecf67a6047476143d15577f",
            },
            TestCase {
                time: 2,
                memory: 64,
                threads: 3,
                hash: "4a15b31aec7c2590b87d1f520be7d96f56658172deaa3079",
            },
            TestCase {
                time: 3,
                memory: 1024,
                threads: 6,
                hash: "1640b932f4b60e272f5d2207b9a9c626ffa1bd88d2349016",
            },
        ];
        let password = b"password";
        let salt = b"somesalt";

        for v in tests {
            let want = hex::decode(v.hash).unwrap();
            let have = key::<24>(password, salt, v.time, v.memory, v.threads);
            assert_eq!(have.as_slice(), want.as_slice());

            let have = key_with_len(password, salt, v.time, v.memory, v.threads, 24);
            assert_eq!(have.as_slice(), want.as_slice());
        }
    }
}