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.