Files
relicario/crates/idfoto-core/tests/integration.rs
adlee-was-taken 7baec1cd67 feat: add group field to Entry and ManifestEntry
Add optional group: Option<String> to both Entry and ManifestEntry for
logical organization (e.g. "work", "personal"). Backwards-compatible via
skip_serializing_if so existing vaults deserialize with group: None.
Includes three new tests verifying round-trip and legacy deserialization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 09:25:18 -04:00

154 lines
5.6 KiB
Rust

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<u8> {
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, &params).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,
group: 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, &params).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, &params).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()),
group: None,
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, &params).unwrap();
// 2. (passphrase_B, image_A) -> different from #1
let key_ba = derive_master_key(passphrase_b, &image_secret_a, &salt, &params).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, &params).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, &params).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");
}