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
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
172 lines
5.8 KiB
Rust
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));
|
|
}
|