Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NIP-103: Onion Routed Direct Messages #499

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions 103.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
NIP-103
=======

Onion Routed Direct Messages
----------------------------

`draft` `optional` `author:threeseries` `author:giszmo`

This NIP defines event kinds 174 and 20174 which are events whose RSA-encrypted content is either a kind 4, (see [NIP-04](04.md), kind 174, or kind 20174 event. A kind 20174 event is nothing more than an ephemeral kind 174 event (kind 20174 can be substituted anywhere kind 174 appears in what follows). These events are intended as direct messages that can be routed through a network of bots or ordinary users to obscure sender and receiver.

# Motivation and usage

Despite being encrypted direct messages on nostr have very poor privacy properties since anyone can see who is messaging whom and when. One solution to this problem is for the entire event including its metadata to be encrypted before being sent, and for the final recipient to be further obfuscated by adding additional hops between sender and receiver. In order to provide additional privacy for users RSA keys are used for encryption since these messages can be decrypted without knowledge of the encrypting user's nostr pubkey.

The flow works as follows: when Bob wishes to send Alice an onion-routed DM he must first identify a set of intermediate pubkeys that can be used for routing and obtain their corresponding RSA public keys. Once done Bob creates a kind 4 event addressed to Alice using his nsec and then encrypts the whole event JSON using Alice's public RSA key. This becomes the content for the outer kind 174 event. The sender of this outer event is not Bob in general, but is rather the pubkey immediately before Alice in the chain. Events are then iterately wrapped in kind 174, working back up the chain until finally reaching Bob.
threeseries marked this conversation as resolved.
Show resolved Hide resolved

When Bob sends this kind 174 event to the first hop in the chain, the user or bot decrypts the content using their private RSA key. The decrypted content will be either kind 174 or kind 4, and the message is forwarded to the recipient pubkey. In order to provide additional privacy time delays can be added, or messages not forwarded until enough are in a queue.

# Intermediate hops

Intermediate nodes can be one of two types: always-online bots that exist solely to perform onion-routing, or ordinary users who have opted into forwarding messages for others (this also provides plausible deniability to the users themselves who are participating in forwarding). In the former case it may be desirable to use kind 20174 to make tracing more difficult, however there needs to be a way for bots to signal that they're online to ensure that such a message will be received. Hence it may be useful to have an ephemeral "heartbeat" event for sending these types of signals.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my proposal I suggested to update a replaceable event regularly. A bot that updated an hour ago is probably online. A heartbeat would have to be every minute or the user firing up his client would have wait a long time before knowing an online bot is online.

You could also probe by sending a message to yourself.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's ephemeral I don't see a huge problem with firing one every few minutes. I like the idea of probing as well, and it seems like that could be a useful tool in some situations.

An issue I see with updating a replaceable event is there'd be fuzziness over how to interpret the timestamp, and knowing how far in the past the event truly was. With an ephemeral event you'd know it was just sent because otherwise you wouldn't have received it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm arguing for lower relay load here. Updating the replaceable event is way more expensive than an ephemeral event but the latter you would need once per minute I would say and I consider the replaceable event to be preferable.

There is network, CPU and storage. 60 ephemeral events would be maybe x60 the network, x20 the CPU and x0 the storage. Not sure what will be the bottleneck.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we prefer replaceable to ephemeral how about reusing kind 0 again? The content could contain a last_updated_at field or something like that which could be used to signal online status (and also just general metadata freshness which seems useful outside of this NIP).


# RSA keys

RSA keys should be derived deterministically from the user's nsec. They should also be advertised in the metadata of a pubkey for any account that can perform onion routing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid you have to be more prescriptive of how this should work. While profiles must expose the public RSA key, clients must know how to independently derive it from the Schnorr key. Some

entropy = sha256(npub+"RSA")
rsa = getRsaKeyPair(entropy)

but you'd have to research this yourself for some real-world code example.

Also you have to prescribe how and where to store the RSA pubkey. In the profile? In a new replaceable event?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might take me some time as I'm not a cryptographer, but I'll give it a shot. Could PGP also work for this? I'm thinking the easiest way to advertise would probably be to put the public key in a kind 0 metadata event.

However the key derviation works, it'd be nice if it enabled a way of verifying whether an RSA or PGP public key actually corresponded to the user's npub (i.e., the private key was derived from the associated nsec).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, the key derivation is not compatible in a sense that an RSA pub key wouldn't have anything to do with the Schnorr pubkey. The determinism is in my opinion only important, so a user doesn't have to backup yet another key. As the event is signed with the Schnorr key though, you can be certain that the RSA key was approved.

Copy link
Author

@threeseries threeseries May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently for RSA it's recommended to have 2048 bit keys based on the prime numbers p and q. One way we could derive them deterministically from a nostr secret key would be to (for example) repeatedly hash the key and stretch it into a 2048 bit number. Then split this into two 1024 pieces and keep incrementing until we hit prime numbers for each. Once we have q and q there are a few more steps that could be implemented by any software: https://en.wikipedia.org/wiki/RSA_(cryptosystem).

Here's some Python code for doing the first part, although it is extremely slow due to the primality test:

from math import sqrt
import hashlib


def is_prime(n: int) -> bool:
    if n <= 1:
        return False
    test = 2
    sqrt_n = sqrt(n)
    while test <= sqrt_n:
        if n % test == 0:
            return False
        test += 1
    return True


result = ""
hex_string = "0487a6b310c7bc874c075001e0aaf492d5d1aae1bfa12cae40fb18497a079027"
for _ in range(8):
    result += hex_string
    m = hashlib.sha256()
    m.update(bytearray.fromhex(hex_string))
    m.digest()
    hex_string = m.hexdigest()

p = int(result[:256], 16)
q = int(result[256:], 16)

while not is_prime(p):
    p += 1
while not is_prime(q):
    q += 1

print(f"p: {p}, q: {q}")

Do you think this might work? Again I don't know much about cryptography and would want to have it reviewed by someone who does.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a cryptographer. If RSA is done by brute-forcing a prime number in the right range, yes, this could work and should not make things slower than any other tool that would work with RSA. Now if we should store the RSA private key in a nip-4 DM style enrypted event I don't know. That would take away the requirement for it to be deterministic.