diff --git a/crates/relicario-core/tests/org.rs b/crates/relicario-core/tests/org.rs new file mode 100644 index 0000000..e780cdd --- /dev/null +++ b/crates/relicario-core/tests/org.rs @@ -0,0 +1,120 @@ +use relicario_core::{ + generate_org_key, wrap_org_key, unwrap_org_key, + encrypt_org_manifest, decrypt_org_manifest, + OrgManifest, OrgManifestEntry, OrgMember, OrgMembers, OrgRole, + MemberId, ItemId, +}; +use relicario_core::item_types::ItemType; +use rand::rngs::OsRng; +use rand::RngCore; +use zeroize::Zeroizing; + +fn make_member_keypair() -> (Zeroizing<[u8; 32]>, String) { + let mut seed = [0u8; 32]; + OsRng.fill_bytes(&mut seed); + let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed); + let pubkey_openssh = ssh_key::PrivateKey::from( + ssh_key::private::Ed25519Keypair::from(&signing_key), + ) + .public_key() + .to_openssh() + .expect("openssh"); + (Zeroizing::new(seed), pubkey_openssh) +} + +#[test] +fn org_key_wrap_unwrap_round_trip() { + let (seed, pubkey) = make_member_keypair(); + let org_key = generate_org_key(); + let wrapped = wrap_org_key(&org_key, &pubkey).expect("wrap"); + let unwrapped = unwrap_org_key(&wrapped, &seed).expect("unwrap"); + assert_eq!(*org_key, *unwrapped); +} + +#[test] +fn revoked_member_cannot_decrypt_after_rotation() { + // Alice and Bob both get access + let (alice_seed, alice_pubkey) = make_member_keypair(); + let (_bob_seed, bob_pubkey) = make_member_keypair(); + + let org_key = generate_org_key(); + let _alice_wrapped = wrap_org_key(&org_key, &alice_pubkey).expect("wrap alice"); + let _bob_wrapped = wrap_org_key(&org_key, &bob_pubkey).expect("wrap bob"); + + // Rotate: new key, only Bob gets re-wrapped + let new_org_key = generate_org_key(); + let new_bob_wrapped = wrap_org_key(&new_org_key, &bob_pubkey).expect("wrap bob new"); + + // Alice tries to use old org_key — she can still decrypt old items, + // but new_bob_wrapped was encrypted with new_org_key, not org_key. + // Verify: unwrapping new_bob_wrapped with Alice's seed fails. + let result = unwrap_org_key(&new_bob_wrapped, &alice_seed); + assert!(result.is_err(), "Alice should not be able to unwrap Bob's new key blob"); +} + +#[test] +fn org_manifest_filter_restricts_to_granted_collections() { + let mut manifest = OrgManifest::new(); + for (title, collection) in &[("A", "prod"), ("B", "dev"), ("C", "prod")] { + manifest.entries.push(OrgManifestEntry { + id: ItemId::new(), + r#type: ItemType::SecureNote, + title: title.to_string(), + tags: vec![], + modified: 0, + trashed_at: None, + collection: collection.to_string(), + }); + } + + let member = OrgMember { + member_id: MemberId::new(), + display_name: "Alice".into(), + role: OrgRole::Member, + ed25519_pubkey: String::new(), + collections: vec!["prod".into()], + added_at: 0, + added_by: MemberId::new(), + }; + + let filtered = manifest.filter_for_member(&member); + assert_eq!(filtered.entries.len(), 2); + assert!(filtered.entries.iter().all(|e| e.collection == "prod")); +} + +#[test] +fn org_manifest_encrypt_decrypt_round_trip() { + let key = generate_org_key(); + let mut manifest = OrgManifest::new(); + manifest.entries.push(OrgManifestEntry { + id: ItemId::new(), + r#type: ItemType::Login, + title: "GitHub".into(), + tags: vec!["work".into()], + modified: 1748000000, + trashed_at: None, + collection: "eng-tools".into(), + }); + + let encrypted = encrypt_org_manifest(&manifest, &key).expect("encrypt"); + let decrypted = decrypt_org_manifest(&encrypted, &key).expect("decrypt"); + + assert_eq!(decrypted.entries.len(), 1); + assert_eq!(decrypted.entries[0].title, "GitHub"); + assert_eq!(decrypted.entries[0].collection, "eng-tools"); +} + +#[test] +fn members_validation_rejects_invalid_id() { + let mut members = OrgMembers::new(); + members.members.push(OrgMember { + member_id: MemberId("not-hex-lol!!".to_string()), + display_name: "Bad".into(), + role: OrgRole::Member, + ed25519_pubkey: String::new(), + collections: vec![], + added_at: 0, + added_by: MemberId::new(), + }); + assert!(members.validate().is_err()); +}