libsoliton/soliton/tests/integration_argon2.rs
Kamal Tufekcic 1d99048c95
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
initial commit
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
2026-04-02 23:48:10 +03:00

172 lines
5.8 KiB
Rust

//! 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));
}