Skip to content

Commit a3438d4

Browse files
committed
Add peer-to-peer chat section
1 parent af821ea commit a3438d4

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- [Rate user](./user_rating.md)
2020
- [Cancel](./cancel.md)
2121
- [Dispute](./dispute.md)
22+
- [Peer-to-peer Chat](./chat.md)
2223
- [List disputes](./list_disputes.md)
2324
- [Admin Settle order](./admin_settle_order.md)
2425
- [Admin Cancel order](./admin_cancel_order.md)

src/chat.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Peer-to-peer Chat
2+
3+
To communicate directly, both the buyer and the seller do not use the current `Message` scheme explained [here](https://mostro.network/protocol/overview.html), as this communication excludes the Mostro daemon. To preserve user privacy, we use a simplified version of NIP-59 that allows us to hide the metadata of both parties from outside observers. However, this variant only contains a single event inside the wrapper. The inner event includes the sender’s trade pubkey and the corresponding signature to maintain the authenticity of the sender.
4+
5+
## Shared Key
6+
7+
The messages between parties have a unique feature: instead of directing the events containing these messages to the counterparty’s trade pubkey, we direct them to a unique pubkey known only to both parties.
8+
9+
We use **Elliptic Curve Diffie-Hellman** (ECDH) to obtain a shared key between the two parties, which in our case serves as a master key to decrypt the wrapper’s content. Either party can voluntarily share this key with the solver in case of a dispute, so the solver can check if someone is lying.
10+
11+
```
12+
Alice Bob
13+
----- -----
14+
Private Key: a Private Key: b
15+
Public Key: A = a * G Public Key: B = b * G
16+
(G is the curve’s base point)
17+
18+
1. Alice sends A to Bob -----> Bob receives A
19+
2. Bob sends B to Alice <----- Alice receives B
20+
21+
Alice computes: Bob computes:
22+
Shared Key = a * B Shared Key = b * A
23+
= a * (b * G) = b * (a * G)
24+
= ab * G = ba * G
25+
= Same Shared Key!
26+
```
27+
28+
## Example:
29+
30+
### 1. Creating the Inner Event
31+
32+
We create a kind 1 event with a message, signed by the author.
33+
34+
```json
35+
{
36+
"id": "<Event Id>",
37+
"pubkey": "<Index N pubkey (trade key)>",
38+
"kind": 1,
39+
"created_at": 1691518405,
40+
"content": "Let’s reestablish the peer-to-peer nature of Bitcoin!",
41+
"tags": [],
42+
"sig": "<Index N (trade key) signature>"
43+
}
44+
```
45+
46+
### 2. Wrapping the Inner Event
47+
48+
We calculate the shared key and encrypt the JSON-encoded kind 1 inner event with the ephemeral key. The result is placed in the content field of a kind 1059 event. We add a p tag containing the shared key’s pubkey and finally sign the event using the random (ephemeral) key.
49+
50+
```json
51+
{
52+
"content": "<Encrypted content>",
53+
"kind": 1059,
54+
"created_at": 1703021488,
55+
"pubkey": "<Ephemeral pubkey>",
56+
"id": "<Event Id>",
57+
"sig": "<Ephemeral key signature>",
58+
"tags": [["p", "<Shared Pubkey>"]]
59+
}
60+
```
61+
62+
## Encrypting Payloads
63+
64+
Encryption is done following [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) on the JSON-encoded inner event. Place the encryption payload in the `content` of the wrapper event.
65+
66+
## Other considerations
67+
68+
Clients MUST attach a certain amount of proof-of-work to the wrapper event per [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) in a bid to demonstrate that the event is not spam or a denial-of-service attack.
69+
70+
The canonical `created_at` time belongs to the inner event. The wrapper timestamp SHOULD be tweaked to thwart time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps SHOULD be in the past.
71+
72+
### Code Example
73+
74+
#### Rust
75+
76+
```rust
77+
use nostr::util::generate_shared_key;
78+
use nostr_sdk::prelude::*;
79+
80+
// Alice
81+
// Hex public key: 000053c3b4773182e7c4c1b72b272d34be01bf4414a6a25c998977c516a46a01
82+
// Hex private key: 548f68890c49fa42f104c60352395e60ff030b0b407e955f1eed1400d6c0347a
83+
// Npub public key: npub1qqq98sa5wucc9e7ycxmjkfedxjlqr06yzjn2yhye39mu294ydgqsf8r490
84+
// Nsec private key: nsec12j8k3zgvf8ay9ugyccp4yw27vrlsxzctgplf2hc7a52qp4kqx3aq0ttwy2
85+
86+
// Bob
87+
// Hex public key: 000009ae5cff9f6ba9b05159ec5ed58c187f5882ea77c81ed5dd19163272a5d7
88+
// Hex private key: f258e73f07386d37133718b6127f873dd7c391b8f43b331ff8254034a13d2943
89+
// Npub public key: npub1qqqqntjul70kh2ds29v7chk43sv87kyzafmus8k4m5v3vvnj5htshl66x6
90+
// Nsec private key: nsec17fvww0c88pknwyehrzmpylu88htu8ydc7sanx8lcy4qrfgfa99psdvrw0q
91+
92+
// Hex Shared PubKey: 27199d5878869ec3b4ae1ad5c2fed88840218a119f9ce892828b950fc96b4829
93+
// Hex Shared private key: def6633a53d07d1e829484c4d4bdbbeed2f4b14c21743e63871c174338e39475
94+
95+
#[tokio::main]
96+
async fn main() -> Result<()> {
97+
// Alice
98+
let alice_keys =
99+
Keys::parse("548f68890c49fa42f104c60352395e60ff030b0b407e955f1eed1400d6c0347a")?;
100+
// Bob
101+
let bob_keys = Keys::parse("f258e73f07386d37133718b6127f873dd7c391b8f43b331ff8254034a13d2943")?;
102+
// Show Alice bech32 public key
103+
let alice_pubkey = alice_keys.public_key();
104+
let alice_secret = alice_keys.secret_key();
105+
println!("Alice PubKey: {}", alice_pubkey);
106+
107+
// Generate shared key for Alice
108+
let shared_key = generate_shared_key(alice_secret, &bob_keys.public_key())?;
109+
let shared_secret_key = SecretKey::from_slice(&shared_key)?;
110+
let shared_keys = Keys::new(shared_secret_key);
111+
println!("Shared PubKey: {}", shared_keys.public_key());
112+
println!(
113+
"Shared private key: {}",
114+
shared_keys.secret_key().to_secret_hex()
115+
);
116+
// Generate shared key for Bob
117+
let bob_shared_key = generate_shared_key(bob_keys.secret_key(), &alice_keys.public_key())?;
118+
// Check if both shared keys are the same, shared keys are not the same it panic
119+
assert_eq!(shared_key, bob_shared_key);
120+
// Show Bob bech32 public key
121+
let bob_pubkey = bob_keys.public_key();
122+
// let bob_secret = bob_keys.secret_key();
123+
println!("Bob PubKey: {}", bob_pubkey);
124+
125+
let message = "Let’s reestablish the peer-to-peer nature of Bitcoin!";
126+
// We encrypt the event to the shared key and only can be decrypted by the shared key
127+
// and sign the inside event with the sender key, in this case Alice
128+
// We do this to ensure that the message is from Alice and only Bob can read it
129+
// But both parties can `shared` the shared key to anyone to decrypt the message
130+
// This is useful for p2p like Mostro where in case of a dispute the message can be decrypted
131+
// by a third party to know if someone is lying
132+
let wrapped_event = mostro_wrap(&alice_keys, shared_keys.public_key(), message, vec![]).await?;
133+
println!("Outer event: {:#?}", wrapped_event);
134+
135+
// We decrypt the event with the shared key
136+
let unwrapped_event = mostro_unwrap(&shared_keys, wrapped_event).await.unwrap();
137+
println!("Inner event: {:#?}", unwrapped_event);
138+
139+
Ok(())
140+
}
141+
142+
/// Wraps a message in a non standard and simplified NIP-59 event.
143+
/// The inner event is signed with the sender's key and encrypted to the receiver's
144+
/// public key using an ephemeral key.
145+
///
146+
/// # Arguments
147+
/// - `sender`: The sender's keys for signing the inner event.
148+
/// - `receiver`: The receiver's public key for encryption.
149+
/// - `message`: The message to wrap.
150+
/// - `extra_tags`: Additional tags to include in the wrapper event.
151+
///
152+
/// # Returns
153+
/// A signed `Event` representing the NON STANDARD gift wrap.
154+
pub async fn mostro_wrap(
155+
sender: &Keys,
156+
receiver: PublicKey,
157+
message: &str,
158+
extra_tags: Vec<Tag>,
159+
) -> Result<Event, Box<dyn std::error::Error>> {
160+
let inner_event = EventBuilder::text_note(message)
161+
.build(sender.public_key())
162+
.sign(sender)
163+
.await?;
164+
let keys: Keys = Keys::generate();
165+
let encrypted_content: String = nip44::encrypt(
166+
keys.secret_key(),
167+
&receiver,
168+
inner_event.as_json(),
169+
nip44::Version::V2,
170+
)
171+
.unwrap();
172+
173+
// Build tags for the wrapper event
174+
let mut tags = vec![Tag::public_key(receiver)];
175+
tags.extend(extra_tags);
176+
177+
// Create and sign the gift wrap event
178+
let wrapped_event = EventBuilder::new(Kind::GiftWrap, encrypted_content)
179+
.tags(tags)
180+
.custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK))
181+
.sign_with_keys(&keys)?;
182+
183+
Ok(wrapped_event)
184+
}
185+
186+
/// Unwraps an non standard NIP-59 event and retrieves the inner event.
187+
/// The receiver uses their private key to decrypt the content.
188+
///
189+
/// # Arguments
190+
/// - `receiver`: The receiver's keys for decryption.
191+
/// - `event`: The wrapped event to unwrap.
192+
///
193+
/// # Returns
194+
/// The decrypted inner `Event`.
195+
pub async fn mostro_unwrap(
196+
receiver: &Keys,
197+
event: Event,
198+
) -> Result<Event, Box<dyn std::error::Error>> {
199+
let decrypted_content = nip44::decrypt(receiver.secret_key(), &event.pubkey, &event.content)?;
200+
let inner_event = Event::from_json(&decrypted_content)?;
201+
202+
// Verify the event before returning
203+
inner_event.verify()?;
204+
205+
Ok(inner_event)
206+
}
207+
```
208+
209+
More details about this implementation can be found in this [repository](https://github.com/grunch/mostro-mediator).

0 commit comments

Comments
 (0)