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