use super::*;
#[test]
fn retry_effects_reuse_the_same_signed_owner_proof_response() {
let owner = Keys::generate();
let device = Keys::generate();
let receiver = Keys::generate();
let mut engine = test_engine(&owner, &device);
engine
.ingest_app_keys_event(&signed_app_keys(&owner, &[device.public_key()], 10))
.unwrap();
let invite = Invite::create_new(receiver.public_key(), None, None).unwrap();
let (_, response) = invite
.accept_with_owner(
device.public_key(),
device.secret_key().to_secret_bytes(),
None,
Some(owner.public_key()),
)
.unwrap();
let prepared = PreparedSend {
recipient_owner: ndr_owner(receiver.public_key()),
payload: vec![],
deliveries: vec![],
invite_responses: vec![response],
relay_gaps: vec![],
};
let build = |engine: &ProtocolEngine| {
let effects =
protocol_effects_from_prepared(&prepared, engine, None, "chat".into(), &mut vec![])
.unwrap();
let ProtocolEffect::Publish(publish) = &effects[0];
publish.event.clone()
};
let original = build(&engine);
for _ in 0..12 {
assert!(
build(&engine) == original,
"re-encrypting the same proof changes the event ID and churns the durable outbox"
);
}
}
#[test]
fn cached_handshake_proof_tracks_roster_changes_and_restart() {
let owner = Keys::generate();
let device = Keys::generate();
let receiver = Keys::generate();
let extra = Keys::generate();
let mut engine = test_engine(&owner, &device);
let proof = signed_app_keys(&owner, &[device.public_key()], 10);
engine.ingest_app_keys_event(&proof).unwrap();
let invite = Invite::create_new(receiver.public_key(), None, None).unwrap();
let (_, response) = invite
.accept_with_owner(
device.public_key(),
device.secret_key().to_secret_bytes(),
None,
Some(owner.public_key()),
)
.unwrap();
let original = engine
.cached_invite_response_with_owner_proof(&response)
.unwrap();
assert_eq!(
handshake_owner_proof(&invite, &original).unwrap().id,
proof.id
);
engine = ProtocolEngine::load_or_create_for_local_device(
engine.storage.clone(),
owner.public_key(),
&device,
)
.unwrap();
let restarted = engine
.cached_invite_response_with_owner_proof(&response)
.unwrap();
assert!(restarted.verify().is_ok());
assert_eq!(
handshake_owner_proof(&invite, &restarted).unwrap().id,
proof.id
);
assert!(
engine
.cached_invite_response_with_owner_proof(&response)
.unwrap()
== restarted
);
let changed = signed_app_keys(&owner, &[device.public_key(), extra.public_key()], 20);
engine.ingest_app_keys_event(&changed).unwrap();
let refreshed = engine
.cached_invite_response_with_owner_proof(&response)
.unwrap();
assert_ne!(refreshed.id, restarted.id);
assert_eq!(
handshake_owner_proof(&invite, &refreshed).unwrap().id,
changed.id
);
engine
.ingest_app_keys_event(&signed_app_keys(&owner, &[extra.public_key()], 30))
.unwrap();
let revoked = engine
.cached_invite_response_with_owner_proof(&response)
.unwrap();
assert!(
handshake_owner_proof(&invite, &revoked).is_none(),
"cached authorization must not outlive device revocation"
);
}
#[test]
fn handshake_proof_cache_bounds_entries_and_bytes() {
let keys = Keys::generate();
let event = nostr::EventBuilder::new(Kind::TextNote, "small")
.sign_with_keys(&keys)
.unwrap();
let key = |index: u64| {
let mut bytes = [0; 32];
bytes[..8].copy_from_slice(&index.to_be_bytes());
(nostr::EventId::from_byte_array(bytes), event.id)
};
let mut cache = HandshakeProofCache::default();
for index in 0..=HANDSHAKE_PROOF_CACHE_ENTRIES {
cache.insert(key(index as u64), event.clone()).unwrap();
}
assert_eq!(cache.events.len(), HANDSHAKE_PROOF_CACHE_ENTRIES);
assert!(!cache.events.contains_key(&key(0)));
assert!(cache
.events
.contains_key(&key(HANDSHAKE_PROOF_CACHE_ENTRIES as u64)));
let large = nostr::EventBuilder::new(Kind::TextNote, "x".repeat(32 * 1024))
.sign_with_keys(&keys)
.unwrap();
for index in 1000..1100 {
cache.insert(key(index), large.clone()).unwrap();
assert!(cache.bytes <= HANDSHAKE_PROOF_CACHE_BYTES);
}
assert!(cache.events.len() < HANDSHAKE_PROOF_CACHE_ENTRIES);
assert!(cache.events.contains_key(&key(1099)));
assert_eq!(cache.events.len(), cache.order.len());
assert_eq!(
cache.bytes,
cache.events.values().map(|(_, size)| size).sum::<usize>()
);
}
#[test]
fn bundled_proof_preserves_released_rust_handshake_parser() {
let owner = Keys::generate();
let device = Keys::generate();
let recipient = Keys::generate();
let mut invite = Invite::create_new(recipient.public_key(), None, None).unwrap();
let (_, response) = invite
.accept_with_owner(
device.public_key(),
device.secret_key().to_secret_bytes(),
None,
Some(owner.public_key()),
)
.unwrap();
let proof = signed_app_keys(&owner, &[device.public_key()], 10);
let event = invite_response_with_owner_proof(&response, Some(&proof)).unwrap();
assert_eq!(event.content, response.content);
let envelope = parse_invite_response_event(&event).unwrap();
let mut rng = OsRng;
let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
let accepted = invite
.process_response(
&mut ctx,
&envelope,
recipient.secret_key().to_secret_bytes(),
)
.unwrap();
assert_eq!(accepted.invitee_identity, device.public_key());
assert_eq!(accepted.owner_public_key, Some(owner.public_key()));
}
#[test]
fn proofless_handshake_keeps_existing_separate_registration_path() {
for proof_first in [false, true] {
let owner = Keys::generate();
let device = Keys::generate();
let recipient_owner = Keys::generate();
let recipient_device = Keys::generate();
let mut recipient = test_engine(&recipient_owner, &recipient_device);
let (_, response) = recipient
.local_invite()
.unwrap()
.accept_with_owner(
device.public_key(),
device.secret_key().to_secret_bytes(),
None,
Some(owner.public_key()),
)
.unwrap();
let event = invite_response_event(&response).unwrap();
let proof = signed_app_keys(&owner, &[device.public_key()], 10);
if proof_first {
recipient.ingest_app_keys_event(&proof).unwrap();
}
recipient.observe_invite_response_event(&event).unwrap();
if !proof_first {
assert_eq!(
recipient.active_session_count_for_owner(owner.public_key()),
0
);
recipient.ingest_app_keys_event(&proof).unwrap();
}
assert_eq!(
recipient.active_session_count_for_owner(owner.public_key()),
1
);
}
}
#[test]
fn linked_device_handshake_authorizes_messages_without_separate_roster_delivery() {
let sender_owner = Keys::generate();
let sender_device = Keys::generate();
let receiver_owner = Keys::generate();
let receiver_device = Keys::generate();
let mut sender = test_engine(&sender_owner, &sender_device);
let mut receiver = test_engine(&receiver_owner, &receiver_device);
sender
.ingest_app_keys_event(&signed_app_keys(
&sender_owner,
&[sender_device.public_key()],
10,
))
.unwrap();
sender
.ingest_app_keys_event(&signed_app_keys(
&receiver_owner,
&[receiver_device.public_key()],
10,
))
.unwrap();
let ProtocolAcceptInviteOutcome::Accepted(accepted) = sender
.accept_invite(
&receiver.local_invite().unwrap(),
Some(receiver_owner.public_key()),
)
.unwrap()
else {
panic!("acceptance should succeed");
};
let response = accepted
.effects
.iter()
.find_map(|effect| match effect {
ProtocolEffect::Publish(publish)
if publish.event.kind.as_u16() as u32 == INVITE_RESPONSE_KIND =>
{
Some(publish.event.clone())
}
_ => None,
})
.unwrap();
receiver.observe_invite_response_event(&response).unwrap();
assert_eq!(
receiver.active_session_count_for_owner(sender_owner.public_key()),
1,
"the encrypted handshake must carry sufficient signed device proof"
);
}
#[test]
fn handshake_proof_delivers_out_of_order_messages_and_survives_restart() {
let sender_owner = Keys::generate();
let sender_device = Keys::generate();
let receiver_owner = Keys::generate();
let receiver_device = Keys::generate();
let sender_store = Arc::new(InMemoryStorage::new());
let receiver_store = Arc::new(InMemoryStorage::new());
let mut sender = ProtocolEngine::load_or_create_for_local_device(
sender_store.clone(),
sender_owner.public_key(),
&sender_device,
)
.unwrap();
let mut receiver = ProtocolEngine::load_or_create_for_local_device(
receiver_store.clone(),
receiver_owner.public_key(),
&receiver_device,
)
.unwrap();
let proof = signed_app_keys(&sender_owner, &[sender_device.public_key()], 10);
sender.ingest_app_keys_event(&proof).unwrap();
sender
.ingest_app_keys_event(&signed_app_keys(
&receiver_owner,
&[receiver_device.public_key()],
10,
))
.unwrap();
sender = ProtocolEngine::load_or_create_for_local_device(
sender_store,
sender_owner.public_key(),
&sender_device,
)
.unwrap();
let ProtocolAcceptInviteOutcome::Accepted(accepted) = sender
.accept_invite(
&receiver.local_invite().unwrap(),
Some(receiver_owner.public_key()),
)
.unwrap()
else {
panic!("accept invite");
};
let response = accepted
.effects
.iter()
.find_map(|effect| match effect {
ProtocolEffect::Publish(publish)
if publish.event.kind.as_u16() as u32 == INVITE_RESPONSE_KIND =>
{
Some(publish.event.clone())
}
_ => None,
})
.unwrap();
let outer = serde_json::to_string(&response).unwrap();
assert!(!outer.contains(&sender_owner.public_key().to_hex()));
assert!(!outer.contains(&sender_device.public_key().to_hex()));
assert!(!outer.contains(&proof.sig.to_string()));
let rumor = nostr::EventBuilder::new(Kind::Custom(14), "message before handshake")
.build(sender_owner.public_key());
let sent = sender
.send_direct_unsigned_event_to_peer_only(
receiver_owner.public_key(),
&receiver_owner.public_key().to_hex(),
rumor,
unix_now(),
)
.unwrap();
let message = sent
.effects
.iter()
.find_map(|effect| match effect {
ProtocolEffect::Publish(publish)
if publish.event.kind.as_u16() as u32 == MESSAGE_EVENT_KIND =>
{
Some(publish.event.clone())
}
_ => None,
})
.unwrap();
assert!(receiver
.process_direct_message_event(&message)
.unwrap()
.is_none());
let batch = receiver.observe_invite_response_event(&response).unwrap();
assert_eq!(batch.direct_messages.len(), 1);
assert_eq!(batch.direct_messages[0].sender, sender_owner.public_key());
assert!(batch.direct_messages[0]
.content
.contains("message before handshake"));
receiver = ProtocolEngine::load_or_create_for_local_device(
receiver_store,
receiver_owner.public_key(),
&receiver_device,
)
.unwrap();
assert_eq!(
receiver.active_session_count_for_owner(sender_owner.public_key()),
1
);
assert!(receiver.has_verified_device_owner_claim(
ndr_owner(sender_owner.public_key()),
ndr_device(sender_device.public_key())
));
}
#[test]
fn handshake_proof_rejects_forgery_wrong_bindings_and_known_revocations() {
let sender_owner = Keys::generate();
let sender_device = Keys::generate();
let receiver_owner = Keys::generate();
let receiver_device = Keys::generate();
let stranger = Keys::generate();
let valid = signed_app_keys(&sender_owner, &[sender_device.public_key()], 10);
let mut forged_json = serde_json::to_value(&valid).unwrap();
forged_json["content"] = serde_json::json!("forged");
let forged: Event = serde_json::from_value(forged_json).unwrap();
let wrong_owner = signed_app_keys(&stranger, &[sender_device.public_key()], 10);
let wrong_device = signed_app_keys(&sender_owner, &[stranger.public_key()], 10);
let future = signed_app_keys(
&sender_owner,
&[sender_device.public_key()],
unix_now().get() + 3600,
);
for (proof, revoked) in [
(&forged, false),
(&wrong_owner, false),
(&wrong_device, false),
(&future, false),
(&valid, true),
] {
let mut receiver = test_engine(&receiver_owner, &receiver_device);
if revoked {
receiver
.ingest_app_keys_event(&signed_app_keys(&sender_owner, &[], 20))
.unwrap();
}
let invite = receiver.local_invite().unwrap();
let (_, response) = invite
.accept_with_owner(
sender_device.public_key(),
sender_device.secret_key().to_secret_bytes(),
None,
Some(sender_owner.public_key()),
)
.unwrap();
let event = invite_response_with_owner_proof(&response, Some(proof)).unwrap();
receiver.observe_invite_response_event(&event).unwrap();
assert_eq!(
receiver.active_session_count_for_owner(sender_owner.public_key()),
0
);
}
}