Skip to main content

mostro_core/chat/
mod.rs

1//! Mostro P2P chat protocol primitives.
2//!
3//! Mostro reuses the NIP-59 GiftWrap envelope to carry a second, lighter
4//! channel: direct buyer/seller chat during a trade and admin/party chat
5//! during a dispute. Unlike protocol messages — which are addressed to a
6//! Mostro node and use the dual identity/trade key scheme of
7//! [`crate::nip59`] — chat envelopes are addressed to a per-channel
8//! **shared key** that both parties derive via ECDH from their trade keys.
9//!
10//! The on-the-wire shape is intentionally simple:
11//!
12//! ```text
13//! Plain-text message
14//!     -> kind 1 TextNote signed by sender_trade_keys (inner)
15//!     -> NIP-44 v2 encrypt to shared_pubkey using an ephemeral key
16//!     -> kind 1059 GiftWrap with `p` = shared_pubkey, signed ephemerally
17//! ```
18//!
19//! Both parties can fetch and decrypt every wrap addressed to the shared
20//! key, and the inner event's signature carries the real sender's trade
21//! pubkey so each side can render the conversation correctly without
22//! exchanging extra metadata.
23//!
24//! This module is **pure protocol**: it derives shared keys, builds and
25//! parses envelopes, and constructs the relay filter. It does not manage
26//! relays, subscriptions, persistence or higher-level workflows — those
27//! belong to the client.
28//!
29//! ## Quick start
30//!
31//! ```no_run
32//! # async fn run() -> Result<(), mostro_core::error::MostroError> {
33//! use mostro_core::chat::{chat_filter, wrap_chat_message, unwrap_chat_message, SharedKey};
34//! use nostr_sdk::prelude::*;
35//!
36//! let alice = Keys::generate();
37//! let bob_pubkey = Keys::generate().public_key();
38//!
39//! let shared = SharedKey::derive(alice.secret_key(), &bob_pubkey)?;
40//! let event = wrap_chat_message(&alice, &shared.public_key(), "hi bob").await?;
41//!
42//! // ...publish `event` to relays, fetch incoming wraps with `chat_filter(...)`,
43//! // then on the receiving side:
44//! let chat = unwrap_chat_message(shared.keys(), &event).await?;
45//! assert_eq!(chat.content, "hi bob");
46//! # Ok(()) }
47//! ```
48
49mod filter;
50mod shared_key;
51mod unwrap;
52mod wrap;
53
54pub use filter::{chat_filter, CHAT_DEFAULT_LOOKBACK_SECS};
55pub use shared_key::SharedKey;
56pub use unwrap::{unwrap_chat_message, ChatMessage};
57pub use wrap::wrap_chat_message;
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use nostr_sdk::nips::nip44;
63    use nostr_sdk::prelude::*;
64
65    fn shared_pair() -> (Keys, Keys, SharedKey, SharedKey) {
66        let alice = Keys::generate();
67        let bob = Keys::generate();
68        let alice_shared = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
69        let bob_shared = SharedKey::derive(bob.secret_key(), &alice.public_key()).unwrap();
70        (alice, bob, alice_shared, bob_shared)
71    }
72
73    #[tokio::test]
74    async fn wrap_and_unwrap_roundtrip() {
75        let (alice, _bob, alice_shared, bob_shared) = shared_pair();
76        let body = "hello from alice";
77
78        let event = wrap_chat_message(&alice, &alice_shared.public_key(), body)
79            .await
80            .expect("wrap");
81
82        assert_eq!(event.kind, Kind::GiftWrap);
83        assert!(event
84            .tags
85            .public_keys()
86            .any(|pk| *pk == alice_shared.public_key()));
87
88        let decoded = unwrap_chat_message(bob_shared.keys(), &event)
89            .await
90            .expect("unwrap");
91
92        assert_eq!(decoded.content, body);
93        assert_eq!(decoded.sender, alice.public_key());
94    }
95
96    #[tokio::test]
97    async fn unwrap_with_wrong_shared_key_fails() {
98        let (alice, _bob, alice_shared, _bob_shared) = shared_pair();
99        let intruder = Keys::generate();
100        let intruder_shared =
101            SharedKey::derive(intruder.secret_key(), &Keys::generate().public_key()).unwrap();
102
103        let event = wrap_chat_message(&alice, &alice_shared.public_key(), "for bob only")
104            .await
105            .expect("wrap");
106
107        let err = unwrap_chat_message(intruder_shared.keys(), &event)
108            .await
109            .expect_err("must not decrypt with foreign shared key");
110        assert!(matches!(
111            err,
112            crate::error::MostroError::MostroInternalErr(_)
113        ));
114    }
115
116    #[tokio::test]
117    async fn unwrap_tampered_event_fails() {
118        let (_alice, _bob, alice_shared, bob_shared) = shared_pair();
119
120        // Build a wrap whose inner ciphertext is the encryption of an event
121        // signed by an *impostor*, not by `alice`. The outer envelope is
122        // perfectly valid (ephemeral key signs it), but the inner signature
123        // must not verify against the rumor pubkey.
124        let impostor = Keys::generate();
125        let inner = EventBuilder::text_note("forged")
126            .build(impostor.public_key())
127            .sign(&impostor)
128            .await
129            .unwrap();
130
131        // Mutate the signed event JSON so the signature no longer matches
132        // its content — `verify()` should reject it.
133        let mut json: serde_json::Value = serde_json::from_str(&inner.as_json()).unwrap();
134        json["content"] = serde_json::Value::String("tampered".to_string());
135        let tampered_inner = json.to_string();
136
137        let ephemeral = Keys::generate();
138        let encrypted = nip44::encrypt(
139            ephemeral.secret_key(),
140            &alice_shared.public_key(),
141            tampered_inner,
142            nip44::Version::V2,
143        )
144        .unwrap();
145        let event = EventBuilder::new(Kind::GiftWrap, encrypted)
146            .tag(Tag::public_key(alice_shared.public_key()))
147            .sign_with_keys(&ephemeral)
148            .unwrap();
149
150        let err = unwrap_chat_message(bob_shared.keys(), &event)
151            .await
152            .expect_err("tampered inner must not verify");
153        assert!(matches!(
154            err,
155            crate::error::MostroError::MostroInternalErr(_)
156        ));
157    }
158
159    #[tokio::test]
160    async fn unwrap_rejects_non_giftwrap_event() {
161        let alice = Keys::generate();
162        let other = Keys::generate();
163        let shared = SharedKey::derive(alice.secret_key(), &other.public_key()).unwrap();
164
165        // A plain text note is the wrong kind for the outer envelope.
166        let bogus = EventBuilder::text_note("not a wrap")
167            .build(alice.public_key())
168            .sign(&alice)
169            .await
170            .unwrap();
171
172        let err = unwrap_chat_message(shared.keys(), &bogus)
173            .await
174            .expect_err("non-giftwrap must error");
175        assert!(matches!(
176            err,
177            crate::error::MostroError::MostroInternalErr(_)
178        ));
179    }
180}