Skip to content

Commit 30ae70e

Browse files
committed
Update inbox.rs
1 parent da19365 commit 30ae70e

File tree

1 file changed

+311
-0
lines changed

1 file changed

+311
-0
lines changed

crates/fula-crypto/src/inbox.rs

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,4 +919,315 @@ mod tests {
919919
let decrypted = loaded.decrypt(recipient.secret_key()).unwrap();
920920
assert_eq!(decrypted.label, Some("Test".to_string()));
921921
}
922+
923+
// ═══════════════════════════════════════════════════════════════════════════
924+
// ISOLATION / LEAKAGE TESTS
925+
// These tests verify that no cross-user data leakage can occur
926+
// ═══════════════════════════════════════════════════════════════════════════
927+
928+
#[test]
929+
fn test_isolation_list_pending_only_shows_own_entries() {
930+
// Scenario: Multiple users have entries in same ShareInbox
931+
// Verify: Each user only sees their own entries via list_pending()
932+
let owner = KekKeyPair::generate();
933+
let user_a = KekKeyPair::generate();
934+
let user_b = KekKeyPair::generate();
935+
let user_c = KekKeyPair::generate();
936+
let dek = DekKey::generate();
937+
938+
let mut inbox = ShareInbox::new();
939+
940+
// Create entries for different users
941+
let token_a = ShareBuilder::new(&owner, user_a.public_key(), &dek)
942+
.path_scope("/for-a/")
943+
.build()
944+
.unwrap();
945+
let envelope_a = ShareEnvelope::new(token_a).with_label("For User A");
946+
inbox.enqueue_share(&envelope_a, user_a.public_key()).unwrap();
947+
948+
let token_b1 = ShareBuilder::new(&owner, user_b.public_key(), &dek)
949+
.path_scope("/for-b-1/")
950+
.build()
951+
.unwrap();
952+
let envelope_b1 = ShareEnvelope::new(token_b1).with_label("For User B - 1");
953+
inbox.enqueue_share(&envelope_b1, user_b.public_key()).unwrap();
954+
955+
let token_b2 = ShareBuilder::new(&owner, user_b.public_key(), &dek)
956+
.path_scope("/for-b-2/")
957+
.build()
958+
.unwrap();
959+
let envelope_b2 = ShareEnvelope::new(token_b2).with_label("For User B - 2");
960+
inbox.enqueue_share(&envelope_b2, user_b.public_key()).unwrap();
961+
962+
// User A should only see 1 entry
963+
let pending_a = inbox.list_pending(&user_a);
964+
assert_eq!(pending_a.len(), 1, "User A should see exactly 1 entry");
965+
assert!(pending_a[0].is_for_recipient(user_a.public_key()));
966+
967+
// User B should see 2 entries
968+
let pending_b = inbox.list_pending(&user_b);
969+
assert_eq!(pending_b.len(), 2, "User B should see exactly 2 entries");
970+
for entry in &pending_b {
971+
assert!(entry.is_for_recipient(user_b.public_key()));
972+
}
973+
974+
// User C should see 0 entries (no shares for them)
975+
let pending_c = inbox.list_pending(&user_c);
976+
assert_eq!(pending_c.len(), 0, "User C should see no entries");
977+
}
978+
979+
#[test]
980+
fn test_isolation_wrong_recipient_cannot_mark_read() {
981+
let owner = KekKeyPair::generate();
982+
let intended_recipient = KekKeyPair::generate();
983+
let attacker = KekKeyPair::generate();
984+
let dek = DekKey::generate();
985+
986+
let mut inbox = ShareInbox::new();
987+
988+
let token = ShareBuilder::new(&owner, intended_recipient.public_key(), &dek)
989+
.build()
990+
.unwrap();
991+
let envelope = ShareEnvelope::new(token);
992+
let entry = inbox.enqueue_share(&envelope, intended_recipient.public_key()).unwrap();
993+
let entry_id = entry.id.clone();
994+
995+
// Attacker tries to mark_read - should fail
996+
let result = inbox.mark_read(&entry_id, attacker.public_key());
997+
assert!(result.is_err(), "Attacker should not be able to mark_read");
998+
999+
// Entry should still be Pending
1000+
let entry = inbox.get_entry(&entry_id).unwrap();
1001+
assert_eq!(entry.status, InboxEntryStatus::Pending, "Status should remain Pending");
1002+
1003+
// Intended recipient can mark_read
1004+
let result = inbox.mark_read(&entry_id, intended_recipient.public_key());
1005+
assert!(result.is_ok(), "Intended recipient should be able to mark_read");
1006+
assert!(result.unwrap(), "mark_read should return true");
1007+
}
1008+
1009+
#[test]
1010+
fn test_isolation_wrong_recipient_cannot_remove() {
1011+
let owner = KekKeyPair::generate();
1012+
let intended_recipient = KekKeyPair::generate();
1013+
let attacker = KekKeyPair::generate();
1014+
let dek = DekKey::generate();
1015+
1016+
let mut inbox = ShareInbox::new();
1017+
1018+
let token = ShareBuilder::new(&owner, intended_recipient.public_key(), &dek)
1019+
.build()
1020+
.unwrap();
1021+
let envelope = ShareEnvelope::new(token);
1022+
let entry = inbox.enqueue_share(&envelope, intended_recipient.public_key()).unwrap();
1023+
let entry_id = entry.id.clone();
1024+
1025+
// Attacker tries to remove - should fail
1026+
let result = inbox.remove_entry(&entry_id, attacker.public_key());
1027+
assert!(result.is_err(), "Attacker should not be able to remove entry");
1028+
1029+
// Entry should still exist
1030+
assert!(inbox.get_entry(&entry_id).is_some(), "Entry should still exist");
1031+
1032+
// Intended recipient can remove
1033+
let result = inbox.remove_entry(&entry_id, intended_recipient.public_key());
1034+
assert!(result.is_ok(), "Intended recipient should be able to remove");
1035+
assert!(result.unwrap().is_some(), "remove_entry should return the entry");
1036+
1037+
// Entry should be gone
1038+
assert!(inbox.get_entry(&entry_id).is_none(), "Entry should be removed");
1039+
}
1040+
1041+
#[test]
1042+
fn test_isolation_wrong_recipient_cannot_accept() {
1043+
let owner = KekKeyPair::generate();
1044+
let intended_recipient = KekKeyPair::generate();
1045+
let attacker = KekKeyPair::generate();
1046+
let dek = DekKey::generate();
1047+
1048+
let mut inbox = ShareInbox::new();
1049+
1050+
let token = ShareBuilder::new(&owner, intended_recipient.public_key(), &dek)
1051+
.path_scope("/secret/")
1052+
.build()
1053+
.unwrap();
1054+
let envelope = ShareEnvelope::new(token).with_label("Secret Data");
1055+
let entry = inbox.enqueue_share(&envelope, intended_recipient.public_key()).unwrap();
1056+
let entry_id = entry.id.clone();
1057+
1058+
// Attacker tries to accept - should fail
1059+
let result = inbox.accept_entry(&entry_id, &attacker);
1060+
assert!(result.is_err(), "Attacker should not be able to accept entry");
1061+
1062+
// Entry should still be Pending (not Accepted)
1063+
let entry = inbox.get_entry(&entry_id).unwrap();
1064+
assert_eq!(entry.status, InboxEntryStatus::Pending, "Status should remain Pending");
1065+
1066+
// Intended recipient can accept
1067+
let result = inbox.accept_entry(&entry_id, &intended_recipient);
1068+
assert!(result.is_ok(), "Intended recipient should be able to accept");
1069+
let envelope = result.unwrap();
1070+
assert_eq!(envelope.label, Some("Secret Data".to_string()));
1071+
}
1072+
1073+
#[test]
1074+
fn test_isolation_entry_content_encrypted_per_recipient() {
1075+
// Scenario: Even if attacker gets the InboxEntry, they cannot decrypt
1076+
let owner = KekKeyPair::generate();
1077+
let intended_recipient = KekKeyPair::generate();
1078+
let attacker = KekKeyPair::generate();
1079+
let dek = DekKey::generate();
1080+
1081+
let token = ShareBuilder::new(&owner, intended_recipient.public_key(), &dek)
1082+
.path_scope("/confidential/")
1083+
.build()
1084+
.unwrap();
1085+
let envelope = ShareEnvelope::new(token).with_label("Confidential");
1086+
let entry = InboxEntry::create(&envelope, intended_recipient.public_key()).unwrap();
1087+
1088+
// Attacker cannot decrypt even with direct access to the entry
1089+
let decrypt_result = entry.decrypt(attacker.secret_key());
1090+
assert!(decrypt_result.is_err(), "Attacker should not be able to decrypt");
1091+
1092+
// Intended recipient can decrypt
1093+
let decrypt_result = entry.decrypt(intended_recipient.secret_key());
1094+
assert!(decrypt_result.is_ok(), "Intended recipient should be able to decrypt");
1095+
let decrypted = decrypt_result.unwrap();
1096+
assert_eq!(decrypted.label, Some("Confidential".to_string()));
1097+
}
1098+
1099+
#[test]
1100+
fn test_isolation_recipient_hash_prevents_enumeration() {
1101+
// Verify that recipient_key_hash correctly identifies ownership
1102+
let user_a = KekKeyPair::generate();
1103+
let user_b = KekKeyPair::generate();
1104+
1105+
// Different users should have different inbox paths
1106+
let path_a = ShareInbox::inbox_path_for_recipient(user_a.public_key());
1107+
let path_b = ShareInbox::inbox_path_for_recipient(user_b.public_key());
1108+
1109+
assert_ne!(path_a, path_b, "Different users should have different inbox paths");
1110+
1111+
// Same user should always get same path (deterministic)
1112+
let path_a2 = ShareInbox::inbox_path_for_recipient(user_a.public_key());
1113+
assert_eq!(path_a, path_a2, "Same user should get same inbox path");
1114+
}
1115+
1116+
#[test]
1117+
fn test_isolation_multi_user_inbox_no_cross_contamination() {
1118+
// Comprehensive test: multiple users, multiple shares, verify complete isolation
1119+
let owner = KekKeyPair::generate();
1120+
let alice = KekKeyPair::generate();
1121+
let bob = KekKeyPair::generate();
1122+
let charlie = KekKeyPair::generate();
1123+
let dek = DekKey::generate();
1124+
1125+
let mut inbox = ShareInbox::new();
1126+
1127+
// Create shares for Alice
1128+
let token_alice = ShareBuilder::new(&owner, alice.public_key(), &dek)
1129+
.path_scope("/alice-secret/")
1130+
.build()
1131+
.unwrap();
1132+
let envelope_alice = ShareEnvelope::new(token_alice).with_label("Alice's Secret");
1133+
let entry_alice = inbox.enqueue_share(&envelope_alice, alice.public_key()).unwrap();
1134+
1135+
// Create shares for Bob
1136+
let token_bob = ShareBuilder::new(&owner, bob.public_key(), &dek)
1137+
.path_scope("/bob-secret/")
1138+
.build()
1139+
.unwrap();
1140+
let envelope_bob = ShareEnvelope::new(token_bob).with_label("Bob's Secret");
1141+
let entry_bob = inbox.enqueue_share(&envelope_bob, bob.public_key()).unwrap();
1142+
1143+
// === VERIFICATION: Alice's operations ===
1144+
1145+
// Alice can see only her entry
1146+
let alice_pending = inbox.list_pending(&alice);
1147+
assert_eq!(alice_pending.len(), 1);
1148+
assert_eq!(alice_pending[0].id, entry_alice.id);
1149+
1150+
// Alice cannot operate on Bob's entry
1151+
assert!(inbox.mark_read(&entry_bob.id, alice.public_key()).is_err());
1152+
assert!(inbox.dismiss_entry(&entry_bob.id, alice.public_key()).is_err());
1153+
assert!(inbox.remove_entry(&entry_bob.id, alice.public_key()).is_err());
1154+
assert!(inbox.accept_entry(&entry_bob.id, &alice).is_err());
1155+
1156+
// Alice can operate on her own entry
1157+
assert!(inbox.accept_entry(&entry_alice.id, &alice).is_ok());
1158+
1159+
// === VERIFICATION: Bob's operations ===
1160+
1161+
// Bob can see only his entry
1162+
let bob_pending = inbox.list_pending(&bob);
1163+
assert_eq!(bob_pending.len(), 1);
1164+
assert_eq!(bob_pending[0].id, entry_bob.id);
1165+
1166+
// Bob cannot operate on Alice's entry (even though Alice already accepted it)
1167+
assert!(inbox.mark_read(&entry_alice.id, bob.public_key()).is_err());
1168+
assert!(inbox.dismiss_entry(&entry_alice.id, bob.public_key()).is_err());
1169+
assert!(inbox.remove_entry(&entry_alice.id, bob.public_key()).is_err());
1170+
1171+
// === VERIFICATION: Charlie (no shares) ===
1172+
1173+
// Charlie sees nothing
1174+
let charlie_pending = inbox.list_pending(&charlie);
1175+
assert_eq!(charlie_pending.len(), 0);
1176+
1177+
// Charlie cannot operate on anyone's entries
1178+
assert!(inbox.mark_read(&entry_alice.id, charlie.public_key()).is_err());
1179+
assert!(inbox.mark_read(&entry_bob.id, charlie.public_key()).is_err());
1180+
assert!(inbox.accept_entry(&entry_bob.id, &charlie).is_err());
1181+
}
1182+
1183+
#[test]
1184+
fn test_isolation_nonexistent_entry_returns_ok_false() {
1185+
// Verify that operations on non-existent entries don't leak information
1186+
// (should return Ok(false) or Ok(None), not Err)
1187+
let user = KekKeyPair::generate();
1188+
let mut inbox = ShareInbox::new();
1189+
1190+
let fake_entry_id = "nonexistent-entry-id-12345";
1191+
1192+
// These should all return Ok with false/None, not error
1193+
// (to prevent timing attacks or enumeration)
1194+
let mark_result = inbox.mark_read(fake_entry_id, user.public_key());
1195+
assert!(mark_result.is_ok(), "Non-existent entry should return Ok");
1196+
assert!(!mark_result.unwrap(), "Should return false for non-existent");
1197+
1198+
let dismiss_result = inbox.dismiss_entry(fake_entry_id, user.public_key());
1199+
assert!(dismiss_result.is_ok(), "Non-existent entry should return Ok");
1200+
assert!(!dismiss_result.unwrap(), "Should return false for non-existent");
1201+
1202+
let remove_result = inbox.remove_entry(fake_entry_id, user.public_key());
1203+
assert!(remove_result.is_ok(), "Non-existent entry should return Ok");
1204+
assert!(remove_result.unwrap().is_none(), "Should return None for non-existent");
1205+
}
1206+
1207+
#[test]
1208+
#[allow(deprecated)]
1209+
fn test_isolation_list_all_deprecation_warning() {
1210+
// This test exists to document that list_all() is deprecated
1211+
// and to verify it still works (for migration purposes)
1212+
let owner = KekKeyPair::generate();
1213+
let user = KekKeyPair::generate();
1214+
let dek = DekKey::generate();
1215+
1216+
let mut inbox = ShareInbox::new();
1217+
1218+
let token = ShareBuilder::new(&owner, user.public_key(), &dek)
1219+
.build()
1220+
.unwrap();
1221+
let envelope = ShareEnvelope::new(token);
1222+
inbox.enqueue_share(&envelope, user.public_key()).unwrap();
1223+
1224+
// list_all() still works but is deprecated
1225+
// Prefer list_pending() with recipient key
1226+
let all = inbox.list_all();
1227+
assert_eq!(all.len(), 1);
1228+
1229+
// Recommended alternative:
1230+
let pending = inbox.list_pending(&user);
1231+
assert_eq!(pending.len(), 1);
1232+
}
9221233
}

0 commit comments

Comments
 (0)