-
Notifications
You must be signed in to change notification settings - Fork 577
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-44: Encrypted Direct Message (Versioned), replaces NIP-4 #574
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
NIP-44 | ||
====== | ||
|
||
Encrypted Direct Message (Versioned) | ||
------------------------------------ | ||
|
||
`optional` `author:paulmillr` | ||
|
||
The NIP replaces NIP4, which is deficient with regards to algorithm choices, and introduces cryptography versioning. NIP4 is potentially vulnerable to [padding oracle attacks](https://en.wikipedia.org/wiki/Padding_oracle_attack) and uses keys which are not indistinguishable from random. | ||
|
||
A special event with kind `44`, meaning "encrypted direct message". It is supposed to have the following attributes: | ||
|
||
**`content`** MUST be equal to the CSV structure `v,param1,param2...`. Different versions may have different param count. First part is always algorithm version, a number. Params of version 1: | ||
|
||
1. `nonce`: base64-encoded xchacha nonce | ||
2. `ciphertext`: base64-encoded xchacha ciphertext, created from (key, nonce) against `plaintext`. | ||
Example: `1,3dBKd83Pg2Q4Tu2A2e8N++c+ZW2IBc2f,FvQi1H4atMwU+FzUR/0CJ7kowjs+` | ||
|
||
**`tags`** MUST contain an entry identifying the receiver of the message (such that relays may naturally forward this event to them), in the form `["p", "<pubkey, as a hex string>"]`. | ||
|
||
**`tags`** MAY contain an entry identifying the previous message in a conversation or a message we are explicitly replying to (such that contextual, more organized conversations may happen), in the form `["e", "<event_id>"]`. | ||
|
||
**Note**: By default in the [libsecp256k1](https://github.com/bitcoin-core/secp256k1) ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). We are using this exact implementation. In NIP4, unhashed shared point was used. | ||
|
||
## Metadata leakage | ||
|
||
All nostr events are passed over the internet, which means some metadata would always leak. | ||
|
||
All compatible clients MUST implement following flow to limit leakage: | ||
|
||
1. Alice authenticates on `wss://nostr.example.com` using NIP-42. This would mean sending and receiving | ||
messages would only be available to proper, authenticated users. | ||
2. On success, Alice creates NIP-65 event specifying `wss://nostr.example.com` as her preferred relay. | ||
The setting is public, and the network would see Alice's preferred DM relays. | ||
Whenever a new user wants to start a conversation with Alice, they would use one of Alice's relays. | ||
|
||
If user did not specify NIP-65 preferred relays, they SHOULD NOT receive direct messages. | ||
|
||
## Versioning | ||
|
||
Clients MUST throw a descriptive error if they receive NIP44 message, version of which they don't support. The error must mention the message version is not supported and suggest appropriate action, such as upgrading, or switching to a different client. | ||
|
||
Currently defined encryption algorithms: | ||
|
||
- `0x00` - RESERVED | ||
- `0x01` - XChaCha with same key `sha256(ecdh)` per conversation | ||
|
||
## Security Warning | ||
|
||
This standard does not go anywhere near what is considered the state-of-the-art in encrypted communication between peers, and it leaks metadata in the events, therefore it must not be used for anything you really need to keep secret, and only with relays that use `AUTH` to restrict who can fetch your `kind:4` events. | ||
|
||
## Client Implementation Warning | ||
|
||
Clients *should not* search and replace public key or note references from the `.content`. If processed like a regular text note (where `@npub...` is replaced with `#[0]` with a `["p", "..."]` tag) the tags are leaked and the mentioned user will receive the message in their inbox. | ||
|
||
## Algorithm | ||
|
||
```js | ||
// npm install @noble/curves @noble/hashes @scure/base @stablelib/xchacha20 | ||
import {xchacha20} from '@noble/ciphers/chacha' | ||
import {secp256k1} from '@noble/curves/secp256k1' | ||
import {sha256} from '@noble/hashes/sha256' | ||
import {randomBytes} from '@noble/hashes/utils' | ||
import {base64} from '@scure/base' | ||
import {utf8Decoder, utf8Encoder} from './utils.ts' | ||
|
||
export function getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { | ||
const key = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB) | ||
return sha256(key.subarray(1, 33)) | ||
} | ||
|
||
export function encrypt( | ||
key: Uint8Array, | ||
text: string, | ||
ver = 1 | ||
): string { | ||
if (ver !== 1) throw new Error('NIP44: unknown encryption version') | ||
let nonce = randomBytes(24) | ||
let plaintext = utf8Encoder.encode(text) | ||
let ciphertext = xchacha20(key, nonce, plaintext, plaintext) | ||
return `1,${base64.encode(nonce)},${base64.encode(ciphertext)}` | ||
} | ||
|
||
export function decrypt(key: Uint8Array, data: string): string { | ||
let dt = data.split(',') | ||
if (dt.length !== 3) throw new Error('NIP44: unknown encryption version'); | ||
let v = Number.parseInt(dt[0]) | ||
if (v !== 1) throw new Error('NIP44: unknown encryption version') | ||
|
||
let nonce = base64.decode(dt[1]) | ||
let ciphertext = base64.decode(dt[2]) | ||
let plaintext = xchacha20(key, nonce, ciphertext) | ||
let text = utf8Decoder.decode(plaintext) | ||
return text | ||
} | ||
``` | ||
paulmillr marked this conversation as resolved.
Show resolved
Hide resolved
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about mentioning that these are Alice's
read
relays, the ones she reads from (in case she uses relays for just read or just write, instead of both).... or better yet, since people are concerned about metadata, we extend NIP-65 to add a flag indicating a DM relay. That way it would be possible for people concerned with metadata leakage to setup a personal relay for receiving DMs and be confident that those DMs are not being read by interlopers (at least the receipient is confident it doesn't leak from their specified read relay, but not that the sender didn't put it elsewhere; and the sender can't really be confident of this, so this is not much of a solution for metadata leakage but it helps a smidgen).