Peer-to-peer Chat
To communicate directly, both the buyer and the seller do not use the current Message
scheme explained here, 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.
Shared Key
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.
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.
Alice Bob
----- -----
Private Key: a Private Key: b
Public Key: A = a * G Public Key: B = b * G
(G is the curve’s base point)
1. Alice sends A to Bob -----> Bob receives A
2. Bob sends B to Alice <----- Alice receives B
Alice computes: Bob computes:
Shared Key = a * B Shared Key = b * A
= a * (b * G) = b * (a * G)
= ab * G = ba * G
= Same Shared Key!
Example:
1. Creating the Inner Event
We create a kind 1 event with a message, signed by the author.
{
"id": "<Event Id>",
"pubkey": "<Index N pubkey (trade key)>",
"kind": 1,
"created_at": 1691518405,
"content": "Let’s reestablish the peer-to-peer nature of Bitcoin!",
"tags": [],
"sig": "<Index N (trade key) signature>"
}
2. Wrapping the Inner Event
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.
{
"content": "<Encrypted content>",
"kind": 1059,
"created_at": 1703021488,
"pubkey": "<Ephemeral pubkey>",
"id": "<Event Id>",
"sig": "<Ephemeral key signature>",
"tags": [["p", "<Shared Pubkey>"]]
}
Encrypting Payloads
Encryption is done following NIP-44 on the JSON-encoded inner event. Place the encryption payload in the content
of the wrapper event.
Other considerations
Clients MUST attach a certain amount of proof-of-work to the wrapper event per NIP-13 in a bid to demonstrate that the event is not spam or a denial-of-service attack.
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.
Code Example
Rust
use nostr::util::generate_shared_key; use nostr_sdk::prelude::*; // Alice // Hex public key: 000053c3b4773182e7c4c1b72b272d34be01bf4414a6a25c998977c516a46a01 // Hex private key: 548f68890c49fa42f104c60352395e60ff030b0b407e955f1eed1400d6c0347a // Npub public key: npub1qqq98sa5wucc9e7ycxmjkfedxjlqr06yzjn2yhye39mu294ydgqsf8r490 // Nsec private key: nsec12j8k3zgvf8ay9ugyccp4yw27vrlsxzctgplf2hc7a52qp4kqx3aq0ttwy2 // Bob // Hex public key: 000009ae5cff9f6ba9b05159ec5ed58c187f5882ea77c81ed5dd19163272a5d7 // Hex private key: f258e73f07386d37133718b6127f873dd7c391b8f43b331ff8254034a13d2943 // Npub public key: npub1qqqqntjul70kh2ds29v7chk43sv87kyzafmus8k4m5v3vvnj5htshl66x6 // Nsec private key: nsec17fvww0c88pknwyehrzmpylu88htu8ydc7sanx8lcy4qrfgfa99psdvrw0q // Hex Shared PubKey: 27199d5878869ec3b4ae1ad5c2fed88840218a119f9ce892828b950fc96b4829 // Hex Shared private key: def6633a53d07d1e829484c4d4bdbbeed2f4b14c21743e63871c174338e39475 #[tokio::main] async fn main() -> Result<()> { // Alice let alice_keys = Keys::parse("548f68890c49fa42f104c60352395e60ff030b0b407e955f1eed1400d6c0347a")?; // Bob let bob_keys = Keys::parse("f258e73f07386d37133718b6127f873dd7c391b8f43b331ff8254034a13d2943")?; // Show Alice bech32 public key let alice_pubkey = alice_keys.public_key(); let alice_secret = alice_keys.secret_key(); println!("Alice PubKey: {}", alice_pubkey); // Generate shared key for Alice let shared_key = generate_shared_key(alice_secret, &bob_keys.public_key())?; let shared_secret_key = SecretKey::from_slice(&shared_key)?; let shared_keys = Keys::new(shared_secret_key); println!("Shared PubKey: {}", shared_keys.public_key()); println!( "Shared private key: {}", shared_keys.secret_key().to_secret_hex() ); // Generate shared key for Bob let bob_shared_key = generate_shared_key(bob_keys.secret_key(), &alice_keys.public_key())?; // Check if both shared keys are the same, shared keys are not the same it panic assert_eq!(shared_key, bob_shared_key); // Show Bob bech32 public key let bob_pubkey = bob_keys.public_key(); // let bob_secret = bob_keys.secret_key(); println!("Bob PubKey: {}", bob_pubkey); let message = "Let’s reestablish the peer-to-peer nature of Bitcoin!"; // We encrypt the event to the shared key and only can be decrypted by the shared key // and sign the inside event with the sender key, in this case Alice // We do this to ensure that the message is from Alice and only Bob can read it // But both parties can `shared` the shared key to anyone to decrypt the message // This is useful for p2p like Mostro where in case of a dispute the message can be decrypted // by a third party to know if someone is lying let wrapped_event = mostro_wrap(&alice_keys, shared_keys.public_key(), message, vec![]).await?; println!("Outer event: {:#?}", wrapped_event); // We decrypt the event with the shared key let unwrapped_event = mostro_unwrap(&shared_keys, wrapped_event).await.unwrap(); println!("Inner event: {:#?}", unwrapped_event); Ok(()) } /// Wraps a message in a non standard and simplified NIP-59 event. /// The inner event is signed with the sender's key and encrypted to the receiver's /// public key using an ephemeral key. /// /// # Arguments /// - `sender`: The sender's keys for signing the inner event. /// - `receiver`: The receiver's public key for encryption. /// - `message`: The message to wrap. /// - `extra_tags`: Additional tags to include in the wrapper event. /// /// # Returns /// A signed `Event` representing the NON STANDARD gift wrap. pub async fn mostro_wrap( sender: &Keys, receiver: PublicKey, message: &str, extra_tags: Vec<Tag>, ) -> Result<Event, Box<dyn std::error::Error>> { let inner_event = EventBuilder::text_note(message) .build(sender.public_key()) .sign(sender) .await?; let keys: Keys = Keys::generate(); let encrypted_content: String = nip44::encrypt( keys.secret_key(), &receiver, inner_event.as_json(), nip44::Version::V2, ) .unwrap(); // Build tags for the wrapper event let mut tags = vec![Tag::public_key(receiver)]; tags.extend(extra_tags); // Create and sign the gift wrap event let wrapped_event = EventBuilder::new(Kind::GiftWrap, encrypted_content) .tags(tags) .custom_created_at(Timestamp::tweaked(nip59::RANGE_RANDOM_TIMESTAMP_TWEAK)) .sign_with_keys(&keys)?; Ok(wrapped_event) } /// Unwraps an non standard NIP-59 event and retrieves the inner event. /// The receiver uses their private key to decrypt the content. /// /// # Arguments /// - `receiver`: The receiver's keys for decryption. /// - `event`: The wrapped event to unwrap. /// /// # Returns /// The decrypted inner `Event`. pub async fn mostro_unwrap( receiver: &Keys, event: Event, ) -> Result<Event, Box<dyn std::error::Error>> { let decrypted_content = nip44::decrypt(receiver.secret_key(), &event.pubkey, &event.content)?; let inner_event = Event::from_json(&decrypted_content)?; // Verify the event before returning inner_event.verify()?; Ok(inner_event) }
More details about this implementation can be found in this repository.