From 6b3edea5d8239bb5df3cfcac58a0ff7467f98396 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 11 Apr 2026 23:13:12 -0400 Subject: [PATCH] test: add full-workflow integration test and two-factor independence verification Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/idfoto-core/tests/integration.rs | 151 ++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 crates/idfoto-core/tests/integration.rs diff --git a/crates/idfoto-core/tests/integration.rs b/crates/idfoto-core/tests/integration.rs new file mode 100644 index 0000000..29fb40e --- /dev/null +++ b/crates/idfoto-core/tests/integration.rs @@ -0,0 +1,151 @@ +use idfoto_core::{ + decrypt_entry, decrypt_manifest, derive_master_key, encrypt_entry, encrypt_manifest, + generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry, +}; +use rand::RngCore; + +fn make_test_jpeg(width: u32, height: u32) -> Vec { + use image::codecs::jpeg::JpegEncoder; + use image::{ImageBuffer, ImageEncoder, Rgb}; + let img = ImageBuffer::from_fn(width, height, |x, y| { + Rgb([ + ((x * 7 + y * 13) % 256) as u8, + ((x * 11 + y * 3) % 256) as u8, + ((x * 5 + y * 17) % 256) as u8, + ]) + }); + let mut buf = Vec::new(); + let encoder = JpegEncoder::new_with_quality(&mut buf, 92); + encoder + .write_image(img.as_raw(), width, height, image::ExtendedColorType::Rgb8) + .unwrap(); + buf +} + +fn fast_params() -> KdfParams { + KdfParams { + argon2_m: 256, + argon2_t: 1, + argon2_p: 1, + } +} + +#[test] +fn full_vault_workflow() { + // 1. Generate carrier JPEG + let carrier = make_test_jpeg(400, 300); + + // 2. Generate random image_secret and embed + let mut image_secret = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut image_secret); + let stego = idfoto_core::imgsecret::embed(&carrier, &image_secret).unwrap(); + + // 3. Extract and verify + let extracted = idfoto_core::imgsecret::extract(&stego).unwrap(); + assert_eq!(extracted, image_secret, "extracted image_secret must match embedded"); + + // 4. Derive master_key with fast params + let passphrase = b"test-passphrase-long-enough"; + let mut salt = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut salt); + let params = fast_params(); + let master_key = derive_master_key(passphrase, &image_secret, &salt, ¶ms).unwrap(); + + // 5. Create and encrypt an Entry + let entry = Entry { + name: "GitHub".to_string(), + url: Some("https://github.com".to_string()), + username: Some("alice".to_string()), + password: "supersecret123!".to_string(), + notes: Some("my main account".to_string()), + totp_secret: None, + created_at: "2024-01-01T00:00:00Z".to_string(), + updated_at: "2024-01-01T00:00:00Z".to_string(), + }; + + let encrypted = encrypt_entry(&master_key, &entry).unwrap(); + + // 6. Decrypt and verify fields match + let decrypted = decrypt_entry(&master_key, &encrypted).unwrap(); + assert_eq!(decrypted.name, "GitHub"); + assert_eq!(decrypted.password, "supersecret123!"); + assert_eq!(decrypted.username, Some("alice".to_string())); + assert_eq!(decrypted.url, Some("https://github.com".to_string())); + assert_eq!(decrypted.notes, Some("my main account".to_string())); + + // 7. Wrong passphrase -> different key -> decrypt fails + let wrong_key = derive_master_key(b"wrong-passphrase-entirely", &image_secret, &salt, ¶ms).unwrap(); + assert!( + decrypt_entry(&wrong_key, &encrypted).is_err(), + "decryption with wrong passphrase must fail" + ); + + // 8. Wrong image_secret -> different key -> decrypt fails + let mut wrong_secret = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut wrong_secret); + // Make sure it's actually different + if wrong_secret == image_secret { + wrong_secret[0] ^= 0xFF; + } + let wrong_key2 = derive_master_key(passphrase, &wrong_secret, &salt, ¶ms).unwrap(); + assert!( + decrypt_entry(&wrong_key2, &encrypted).is_err(), + "decryption with wrong image_secret must fail" + ); + + // 9. Manifest round-trip + let entry_id = generate_entry_id(); + let mut manifest = Manifest::new(); + manifest.add_entry( + entry_id.clone(), + ManifestEntry { + name: "GitHub".to_string(), + url: Some("https://github.com".to_string()), + username: Some("alice".to_string()), + updated_at: "2024-01-01T00:00:00Z".to_string(), + }, + ); + + let manifest_enc = encrypt_manifest(&master_key, &manifest).unwrap(); + let manifest_dec = decrypt_manifest(&master_key, &manifest_enc).unwrap(); + + assert_eq!(manifest_dec.version, 1); + assert!(manifest_dec.entries.contains_key(&entry_id)); + assert_eq!(manifest_dec.entries[&entry_id].name, "GitHub"); +} + +#[test] +fn two_factor_independence() { + let mut salt = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut salt); + let params = fast_params(); + + let passphrase_a = b"passphrase-alpha"; + let passphrase_b = b"passphrase-bravo"; + + let mut image_secret_a = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut image_secret_a); + let mut image_secret_b = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut image_secret_b); + // Ensure they differ + if image_secret_a == image_secret_b { + image_secret_b[0] ^= 0xFF; + } + + // 1. (passphrase_A, image_A) + let key_aa = derive_master_key(passphrase_a, &image_secret_a, &salt, ¶ms).unwrap(); + + // 2. (passphrase_B, image_A) -> different from #1 + let key_ba = derive_master_key(passphrase_b, &image_secret_a, &salt, ¶ms).unwrap(); + assert_ne!(key_aa, key_ba, "different passphrase must produce different key"); + + // 3. (passphrase_A, image_B) -> different from #1 + let key_ab = derive_master_key(passphrase_a, &image_secret_b, &salt, ¶ms).unwrap(); + assert_ne!(key_aa, key_ab, "different image_secret must produce different key"); + + // 4. (passphrase_B, image_B) -> different from all above + let key_bb = derive_master_key(passphrase_b, &image_secret_b, &salt, ¶ms).unwrap(); + assert_ne!(key_bb, key_aa, "key_bb must differ from key_aa"); + assert_ne!(key_bb, key_ba, "key_bb must differ from key_ba"); + assert_ne!(key_bb, key_ab, "key_bb must differ from key_ab"); +}