Skip to main content

iris_chat_protocol/protocol_engine/
engine_handshake_proof.rs

1use super::*;
2
3// Optional encrypted extension on the existing signed invite response. Keeping
4// the original content unchanged lets older clients continue parsing it.
5const HANDSHAKE_OWNER_PROOF_TAG: &str = "owner-proof";
6const HANDSHAKE_OWNER_PROOF_MAX_BYTES: usize = 32 * 1024;
7
8impl ProtocolEngine {
9    /// Whether retained, verified identity-signed evidence authorizes this device.
10    pub fn signed_local_device_authorization(&self) -> Option<bool> {
11        self.invite_owner_app_keys_evidence
12            .get(&self.local_owner)
13            .map(|_| self.local_handshake_owner_proof().is_some())
14    }
15
16    pub(super) fn local_handshake_owner_proof(&self) -> Option<&Event> {
17        let ProtocolAppKeysEvidence::Verified(event) =
18            self.invite_owner_app_keys_evidence.get(&self.local_owner)?
19        else {
20            return None;
21        };
22        let device = public_device(self.local_device).ok()?;
23        self.invite_owner_exact_app_keys_membership(self.local_owner, device)
24            .filter(|authorized| *authorized)?;
25        Some(event)
26    }
27
28    /// Read the account-signed authorization carried by a successfully decrypted
29    /// handshake. The caller supplies the owner and device authenticated by that
30    /// handshake, so unrelated signed records cannot establish its sender.
31    pub fn ingest_invite_response_owner_proof(
32        &mut self,
33        invite: &Invite,
34        response: &Event,
35        claimed_owner: PublicKey,
36        authenticated_device: PublicKey,
37    ) -> anyhow::Result<ProtocolRetryBatch> {
38        let Some(proof) = handshake_owner_proof(invite, response)
39            .filter(|proof| proof.pubkey == claimed_owner)
40            .filter(|proof| {
41                AppKeys::from_event(proof)
42                    .is_ok_and(|keys| keys.get_device(&authenticated_device).is_some())
43            })
44        else {
45            return Ok(ProtocolRetryBatch::default());
46        };
47        // The normal ingestion path retains newer revocations and conflicting
48        // heads, persists exact signed evidence, and retries parked messages.
49        self.ingest_app_keys_event(&proof)
50    }
51}
52
53pub(super) fn invite_response_with_owner_proof(
54    response: &nostr_double_ratchet::InviteResponseEnvelope,
55    proof: Option<&Event>,
56) -> anyhow::Result<Event> {
57    let event = invite_response_event(response)?;
58    let Some(proof) = proof else {
59        return Ok(event);
60    };
61    let json = serde_json::to_string(proof)?;
62    anyhow::ensure!(
63        json.len() <= HANDSHAKE_OWNER_PROOF_MAX_BYTES,
64        "owner proof too large"
65    );
66    let keys = Keys::new(nostr::SecretKey::from_slice(&response.signer_secret_key)?);
67    let encrypted = nostr::nips::nip44::encrypt(
68        keys.secret_key(),
69        &public_device(response.recipient)?,
70        json,
71        nostr::nips::nip44::Version::V2,
72    )?;
73    Ok(nostr::EventBuilder::new(event.kind, event.content.clone())
74        .tags(event.tags.iter().cloned())
75        .tag(nostr::Tag::parse([
76            HANDSHAKE_OWNER_PROOF_TAG,
77            encrypted.as_str(),
78        ])?)
79        .custom_created_at(event.created_at)
80        .sign_with_keys(&keys)?)
81}
82
83pub(super) fn handshake_owner_proof(invite: &Invite, response: &Event) -> Option<Event> {
84    // The optional extension never replaces validation of the original handshake.
85    let envelope = parse_invite_response_event(response).ok()?;
86    if envelope.recipient != invite.inviter_ephemeral_public_key {
87        return None;
88    }
89    let mut tags = response.tags.iter().filter(|tag| {
90        tag.as_slice()
91            .first()
92            .is_some_and(|name| name == HANDSHAKE_OWNER_PROOF_TAG)
93    });
94    let tag = tags.next()?.as_slice();
95    if tag.len() != 2 || tags.next().is_some() || tag[1].len() > HANDSHAKE_OWNER_PROOF_MAX_BYTES * 2
96    {
97        return None;
98    }
99    let secret = nostr::SecretKey::from_slice(&invite.inviter_ephemeral_private_key?).ok()?;
100    let json = nostr::nips::nip44::decrypt(&secret, &response.pubkey, &tag[1]).ok()?;
101    if json.len() > HANDSHAKE_OWNER_PROOF_MAX_BYTES {
102        return None;
103    }
104    let proof: Event = serde_json::from_str(&json).ok()?;
105    app_keys_event_is_acceptable_for_owner(proof.pubkey, &proof, unix_now().get()).then_some(proof)
106}