libsoliton 0.1.3

Core cryptographic library for the LO protocol — hybrid post-quantum key exchange, signatures, ratchet, and storage encryption
Documentation
//! Integration tests for Argon2id key derivation.
//!
//! Exercises the full passphrase → key → encrypt/decrypt cycle that matches
//! the primary use case: protecting identity keypairs with a user passphrase.

use soliton::primitives::aead::{aead_decrypt, aead_encrypt};
use soliton::primitives::argon2::{Argon2Params, argon2id};
use soliton::primitives::random::random_array;

/// Fast parameters for tests — not for production use.
///
/// m=8 KiB (minimum), t=1, p=1.
const FAST: Argon2Params = Argon2Params {
    m_cost: 8,
    t_cost: 1,
    p_cost: 1,
};

/// Derives a 32-byte key from a passphrase, encrypts a payload,
/// re-derives the same key, and decrypts — verifying round-trip correctness.
#[test]
fn passphrase_protects_keypair() {
    let passphrase = b"correct horse battery staple";
    let salt: [u8; 16] = random_array();
    let nonce: [u8; 24] = random_array();

    // Simulate stored keypair bytes.
    let keypair_bytes = b"secret identity keypair bytes (not real, just for test)";

    // Derive encryption key from passphrase.
    let mut enc_key = [0u8; 32];
    argon2id(passphrase, &salt, FAST, &mut enc_key).unwrap();

    // Encrypt the keypair.
    let ciphertext = aead_encrypt(&enc_key, &nonce, keypair_bytes, b"").unwrap();

    // Re-derive with the same passphrase + salt → identical key.
    let mut dec_key = [0u8; 32];
    argon2id(passphrase, &salt, FAST, &mut dec_key).unwrap();
    assert_eq!(enc_key, dec_key);

    // Decrypt and verify.
    let plaintext = aead_decrypt(&dec_key, &nonce, &ciphertext, b"").unwrap();
    assert_eq!(&*plaintext, keypair_bytes.as_slice());
}

/// A wrong passphrase derives a different key, causing AEAD authentication failure.
#[test]
fn wrong_passphrase_fails_decryption() {
    let salt: [u8; 16] = random_array();
    let nonce: [u8; 24] = random_array();

    let mut key_correct = [0u8; 32];
    argon2id(b"correct passphrase", &salt, FAST, &mut key_correct).unwrap();

    let ciphertext = aead_encrypt(&key_correct, &nonce, b"secret keypair", b"").unwrap();

    let mut key_wrong = [0u8; 32];
    argon2id(b"wrong passphrase", &salt, FAST, &mut key_wrong).unwrap();

    assert!(
        aead_decrypt(&key_wrong, &nonce, &ciphertext, b"").is_err(),
        "decryption with wrong passphrase must fail"
    );
}

/// A different salt (e.g. from a different device or key slot) produces a
/// different derived key, causing AEAD authentication failure.
#[test]
fn wrong_salt_fails_decryption() {
    let salt_a = [0x11u8; 16];
    let salt_b = [0x22u8; 16];
    let nonce: [u8; 24] = random_array();

    let mut key_a = [0u8; 32];
    argon2id(b"passphrase", &salt_a, FAST, &mut key_a).unwrap();

    let ciphertext = aead_encrypt(&key_a, &nonce, b"secret keypair", b"").unwrap();

    let mut key_b = [0u8; 32];
    argon2id(b"passphrase", &salt_b, FAST, &mut key_b).unwrap();

    assert!(
        aead_decrypt(&key_b, &nonce, &ciphertext, b"").is_err(),
        "decryption with different salt must fail"
    );
}

/// An empty passphrase (`b""`) is valid input — Argon2id accepts zero-length
/// passwords and returns deterministic output.
#[test]
fn empty_passphrase_is_accepted() {
    let salt: [u8; 16] = [0x01u8; 16];
    let mut out1 = [0u8; 32];
    let mut out2 = [0u8; 32];
    // Must not error — empty passphrase is explicitly allowed by Argon2 spec.
    argon2id(b"", &salt, FAST, &mut out1).expect("empty passphrase must be accepted");
    argon2id(b"", &salt, FAST, &mut out2).unwrap();
    // Must be deterministic.
    assert_eq!(
        out1, out2,
        "empty passphrase must produce deterministic output"
    );
    // Must differ from a non-empty passphrase.
    let mut out_nonempty = [0u8; 32];
    argon2id(b"x", &salt, FAST, &mut out_nonempty).unwrap();
    assert_ne!(
        out1, out_nonempty,
        "empty and non-empty passphrases must produce different keys"
    );
}

/// A salt shorter than the minimum valid length (8 bytes for Argon2) must
/// return a clear error rather than panicking or silently producing output.
#[test]
fn short_salt_returns_error() {
    use soliton::error::Error;
    let mut out = [0u8; 32];
    let result = argon2id(b"passphrase", b"short", FAST, &mut out);
    assert!(
        matches!(
            result,
            Err(Error::InvalidLength {
                expected: 8,
                got: 5
            })
        ),
        "expected InvalidLength {{ expected: 8, got: 5 }}, got: {:?}",
        result,
    );
}

/// Verifies that `Argon2Params::OWASP_MIN` and `Argon2Params::RECOMMENDED`
/// constants compile and produce non-zero output.
///
/// These use real cost parameters and will run slower than other tests (~0.5-2 s
/// depending on hardware). The `#[ignore]` attribute keeps them out of `cargo test`
/// by default; run with `cargo test -- --ignored` to execute.
#[test]
#[ignore = "slow: uses production-grade Argon2 parameters (~0.5-2 s)"]
fn recommended_params_produce_output() {
    let mut out = [0u8; 32];
    argon2id(
        b"passphrase",
        b"saltsaltsaltsalt",
        Argon2Params::RECOMMENDED,
        &mut out,
    )
    .unwrap();
    assert!(out.iter().any(|&b| b != 0));
}

/// Verifies that `Argon2Params::OWASP_MIN` (19 MiB, t=2, p=1) compiles and
/// produces non-zero output. OWASP_MIN is the absolute minimum recommended for
/// new applications; this test confirms the preset is correctly defined and
/// accepted by the Argon2id implementation.
///
/// Uses `#[ignore]` to keep it out of normal CI — run with
/// `cargo test -- --ignored` to execute.
#[test]
#[ignore = "slow: uses OWASP minimum Argon2 parameters (~19 MiB, t=2, ~0.5 s)"]
fn owasp_min_params_produce_output() {
    let mut out = [0u8; 32];
    argon2id(
        b"passphrase",
        b"saltsaltsaltsalt",
        Argon2Params::OWASP_MIN,
        &mut out,
    )
    .unwrap();
    assert!(out.iter().any(|&b| b != 0));
}