initial commit
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
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>
This commit is contained in:
commit
1d99048c95
165830 changed files with 79062 additions and 0 deletions
172
soliton/tests/integration_argon2.rs
Normal file
172
soliton/tests/integration_argon2.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
//! 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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue