diff options
| author | Jon Staab <shtaab@gmail.com> | 2023-12-20 11:28:43 -0800 |
|---|---|---|
| committer | Jon Staab <shtaab@gmail.com> | 2023-12-20 11:28:43 -0800 |
| commit | 732b0ce0a49fbdfa35dfae164f25ee9db947f1c2 (patch) | |
| tree | 3f26d4eb5ee98cef3f1e8c32ab0a05e6268382ae | |
| parent | ff533d7a99715b0524f93a182f6c2169fe3b25b1 (diff) | |
Clean up NIP 44 to clarify separation of concerns (encryption vs messaging), improve formatting and clarify encryption/decryption steps
| -rw-r--r-- | 44.md | 239 |
1 files changed, 127 insertions, 112 deletions
| @@ -1,74 +1,146 @@ | |||
| 1 | # NIP-44 | 1 | NIP-44 |
| 2 | ===== | ||
| 2 | 3 | ||
| 3 | ## Encrypted Payloads (Versioned) | 4 | Encrypted Payloads (Versioned) |
| 5 | ------------------------------ | ||
| 4 | 6 | ||
| 5 | `optional` | 7 | `optional` |
| 6 | 8 | ||
| 7 | The NIP introduces a new data format for keypair-based encryption. This NIP is versioned | 9 | The NIP introduces a new data format for keypair-based encryption. This NIP is versioned |
| 8 | to allow multiple algorithm choices to exist simultaneously. | 10 | to allow multiple algorithm choices to exist simultaneously. This format may be used for |
| 11 | many things, but MUST be used in the context of a signed event as described in NIP 01. | ||
| 9 | 12 | ||
| 10 | Nostr is a key directory. Every nostr user has their own public key, which solves key | 13 | ## Versions |
| 11 | distribution problems present in other solutions. The goal of this NIP is to have a | ||
| 12 | simple way to send messages between nostr accounts that cannot be read by everyone. | ||
| 13 | |||
| 14 | The scheme has a number of important shortcomings: | ||
| 15 | 14 | ||
| 16 | - No deniability: it is possible to prove the event was signed by a particular key | 15 | Currently defined encryption algorithms: |
| 17 | - No forward secrecy: when a user key is compromised, it is possible to decrypt all previous conversations | ||
| 18 | - No post-compromise security: when a user key is compromised, it is possible to decrypt all future conversations | ||
| 19 | - No post-quantum security: a powerful quantum computer would be able to decrypt the messages | ||
| 20 | - IP address leak: user IP may be seen by relays and all intermediaries between user and relay | ||
| 21 | - Date leak: the message date is public, since it is a part of NIP 01 event | ||
| 22 | - Limited message size leak: padding only partially obscures true message length | ||
| 23 | - No attachments: they are not supported | ||
| 24 | 16 | ||
| 25 | Lack of forward secrecy is partially mitigated by these two factors: | 17 | - `0x00` - Reserved |
| 26 | 1. the messages should only be stored on relays, specified by the user, instead of a set of all public relays. | 18 | - `0x01` - Deprecated and undefined |
| 27 | 2. the relays are supposed to regularly delete older messages. | 19 | - `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64 |
| 28 | 20 | ||
| 29 | For risky situations, users should chat in specialized E2EE messaging software and limit use of nostr to exchanging contacts. | 21 | ## Limitations |
| 30 | 22 | ||
| 31 | ## Dependence on NIP-01 | 23 | Every nostr user has their own public key, which solves key distribution problems present |
| 24 | in other solutions. However, nostr's relay-based architecture makes it difficult to implement | ||
| 25 | more robust private messaging protocols with things like metadata hiding, forward secrecy, | ||
| 26 | and post compromise secrecy. | ||
| 32 | 27 | ||
| 33 | It's not enough to use NIP-44 for encryption: the output must also be signed. | 28 | The goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed |
| 29 | event. When applying this NIP to any use case, it's important to keep in mind your users' threat | ||
| 30 | model and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE | ||
| 31 | messaging software and limit use of nostr to exchanging contacts. | ||
| 34 | 32 | ||
| 35 | In nostr case, the payload is serialized and signed as per NIP-01 rules. | 33 | On its own, messages sent using this scheme has a number of important shortcomings: |
| 36 | 34 | ||
| 37 | The same event can be serialized in two different ways, resulting in two distinct signatures. So, it's important to ensure serialization rules, which are defined in NIP-01, are the same across different NIP-44 implementations. | 35 | - No deniability: it is possible to prove an event was signed by a particular key |
| 36 | - No forward secrecy: when a key is compromised, it is possible to decrypt all previous conversations | ||
| 37 | - No post-compromise security: when a key is compromised, it is possible to decrypt all future conversations | ||
| 38 | - No post-quantum security: a powerful quantum computer would be able to decrypt the messages | ||
| 39 | - IP address leak: user IP may be seen by relays and all intermediaries between user and relay | ||
| 40 | - Date leak: `created_at` is public, since it is a part of NIP 01 event | ||
| 41 | - Limited message size leak: padding only partially obscures true message length | ||
| 42 | - No attachments: they are not supported | ||
| 38 | 43 | ||
| 39 | After serialization, the event is signed by Schnorr signature over secp256k1, defined in BIP340. It's important to ensure the key and signature validity as per BIP340 rules. | 44 | Lack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking |
| 45 | relays to delete stored messages after a certain duration has elapsed. | ||
| 40 | 46 | ||
| 41 | ## Versions | 47 | ## Version 2 |
| 42 | 48 | ||
| 43 | Currently defined encryption algorithms: | 49 | NIP-44 version 2 has the following design characteristics: |
| 50 | |||
| 51 | - Payloads are authenticated using a MAC before signing rather than afterwards because events are assumed | ||
| 52 | to be signed as specified in NIP-01. The outer signature serves to authenticate the full payload, and MUST | ||
| 53 | be validated before decrypting. | ||
| 54 | - ChaCha is used instead of AES because it's faster and has | ||
| 55 | [better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/). | ||
| 56 | - ChaCha is used instead of XChaCha because XChaCha has not been standardized. Also, xChaCha's improved collision | ||
| 57 | resistance of nonces isn't necessary since every message has a new (key, nonce) pair. | ||
| 58 | - HMAC-SHA256 is used instead of Poly1305 because polynomial MACs are much easier to forge. | ||
| 59 | - SHA256 is used instead of SHA3 or BLAKE because it is already used in nostr. Also BLAKE's speed advantage | ||
| 60 | is smaller in non-parallel environments. | ||
| 61 | - A custom padding scheme is used instead of padmé because it provides better leakage reduction for small messages. | ||
| 62 | - Base64 encoding is used instead of another compression algorithm because it is widely available, and is already used in nostr. | ||
| 63 | |||
| 64 | ### Encryption | ||
| 65 | |||
| 66 | 1. Calculate a conversation key | ||
| 67 | - Execute ECDH (scalar multiplication) of public key B by private key A | ||
| 68 | Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point | ||
| 69 | - Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')` | ||
| 70 | - HKDF output will be a `conversation_key` between two users. | ||
| 71 | - It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)` | ||
| 72 | 2. Generate a random 32-byte nonce | ||
| 73 | - Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) | ||
| 74 | - Don't generate a nonce from message content | ||
| 75 | - Don't re-use the same nonce between messages: doing so would make them decryptable, | ||
| 76 | but won't leak the long-term key | ||
| 77 | 3. Calculate message keys | ||
| 78 | - The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes long | ||
| 79 | - Use HKDF-expand, with sha256, `OKM=conversation_key`, `info=nonce` and `L=76` | ||
| 80 | - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76) | ||
| 81 | 4. Add padding | ||
| 82 | - Content must be encoded from UTF-8 into byte array | ||
| 83 | - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes | ||
| 84 | - Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]` | ||
| 85 | - Padding algorithm is related to powers-of-two, with min padded msg size of 32 | ||
| 86 | - Plaintext length is encoded in big-endian as first 2 bytes of the padded blob | ||
| 87 | 5. Encrypt padded content | ||
| 88 | - Use ChaCha20, with key and nonce from step 3 | ||
| 89 | 6. Calculate MAC (message authentication code) | ||
| 90 | - AAD (additional authenticated data) is used - instead of calculating MAC on ciphertext, | ||
| 91 | it's calculated over a concatenation of `nonce` and `ciphertext` | ||
| 92 | - Validate that AAD (nonce) is 32 bytes | ||
| 93 | 7. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)` | ||
| 44 | 94 | ||
| 45 | - `0x00` - Reserved | 95 | Encrypted payloads MUST be included in an event's payload, hashed, and signed as defined in NIP 01, using schnorr |
| 46 | - `0x01` - Deprecated and undefined | 96 | signature scheme over secp256k1. |
| 47 | - `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64 | ||
| 48 | 97 | ||
| 49 | ## Version 2 | 98 | ### Decryption |
| 50 | 99 | ||
| 51 | The algorithm choices are justified in a following way: | 100 | Before decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be |
| 101 | a valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact | ||
| 102 | validation rules, refer to BIP-340. | ||
| 52 | 103 | ||
| 53 | - Encrypt-then-mac-then-sign instead of encrypt-then-sign-then-mac: only events wrapped in NIP-01 signed envelope are currently accepted by nostr. | 104 | 1. Check if first payload's character is `#` |
| 54 | - ChaCha instead of AES: it's faster and has [better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/) | 105 | - `#` is an optional future-proof flag that means non-base64 encoding is used |
| 55 | - ChaCha instead of XChaCha: XChaCha has not been standardized. Also, we don't need xchacha's improved collision resistance of nonces: every message has a new (key, nonce) pair. | 106 | - The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`, |
| 56 | - HMAC-SHA256 instead of Poly1305: polynomial MACs are much easier to forge SHA256 instead of SHA3 or BLAKE: it is already used in nostr. Also blake's | 107 | implementations MUST indicate that the encryption version is not yet supported |
| 57 | speed advantage is smaller in non-parallel environments - Custom padding instead of padmé: better leakage reduction for small messages | 108 | 2. Decode base64 |
| 58 | - Base64 encoding instead of an other compression algorithm: it is widely available, and is already used in nostr | 109 | - Base64 is decoded into `version, nonce, ciphertext, mac` |
| 110 | - If the version is unknown, implementations must indicate that the encryption version is not supported | ||
| 111 | - Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars | ||
| 112 | - Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes | ||
| 113 | 3. Calculate conversation key | ||
| 114 | - See step 1 of (encryption)[#Encryption] | ||
| 115 | 4. Calculate message keys | ||
| 116 | - See step 3 of (encryption)[#Encryption] | ||
| 117 | 5. Calculate MAC (message authentication code) with AAD and compare | ||
| 118 | - Stop and throw an error if MAC doesn't match the decoded one from step 2 | ||
| 119 | - Use constant-time comparison algorithm | ||
| 120 | 6. Decrypt ciphertext | ||
| 121 | - Use ChaCha20 with key and nonce from step 3 | ||
| 122 | 7. Remove padding | ||
| 123 | - Read the first two BE bytes of plaintext that correspond to plaintext length | ||
| 124 | - Verify that the length of sliced plaintext matches the value of the two BE bytes | ||
| 125 | - Verify that calculated padding from step 3 of the (encryption)[#Encryption] process matches the actual padding | ||
| 59 | 126 | ||
| 60 | ### Functions and operations | 127 | ### Details |
| 61 | 128 | ||
| 62 | - Cryptographic methods | 129 | - Cryptographic methods |
| 63 | - `secure_random_bytes(length)` fetches randomness from CSPRNG | 130 | - `secure_random_bytes(length)` fetches randomness from CSPRNG. |
| 64 | - `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869) with SHA256 hash function, | 131 | - `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869) |
| 65 | comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)` | 132 | with SHA256 hash function comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)`. |
| 66 | - `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439), with starting counter set to 0 | 133 | - `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439) with |
| 67 | - `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104) | 134 | starting counter set to 0. |
| 68 | - `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a ⋅ B`), defined in [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki). The operation produces shared point, and we encode the shared point's 32-byte x coordinate, using method `bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid, on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]` | 135 | - `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104). |
| 136 | - `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a ⋅ B`), defined in | ||
| 137 | [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki). | ||
| 138 | The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method | ||
| 139 | `bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid, | ||
| 140 | on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]`. | ||
| 69 | - Operators | 141 | - Operators |
| 70 | - `x[i:j]`, where `x` is a byte array and `i, j <= 0`, | 142 | - `x[i:j]`, where `x` is a byte array and `i, j <= 0` returns a `(j - i)`-byte array with a copy of the |
| 71 | returns a `(j - i)`-byte array with a copy of the `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x` | 143 | `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`. |
| 72 | - Constants `c`: | 144 | - Constants `c`: |
| 73 | - `min_plaintext_size` is 1. 1b msg is padded to 32b. | 145 | - `min_plaintext_size` is 1. 1b msg is padded to 32b. |
| 74 | - `max_plaintext_size` is 65535 (64kb - 1). It is padded to 65536. | 146 | - `max_plaintext_size` is 65535 (64kb - 1). It is padded to 65536. |
| @@ -82,7 +154,10 @@ The algorithm choices are justified in a following way: | |||
| 82 | - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros | 154 | - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros |
| 83 | - `floor(number)` and `log2(number)` are well-known mathematical methods | 155 | - `floor(number)` and `log2(number)` are well-known mathematical methods |
| 84 | 156 | ||
| 85 | User-defined functions: | 157 | ### Implementation pseudocode |
| 158 | |||
| 159 | The following is a collection of python-like pseudocode functions which implement the above primitives, | ||
| 160 | intended to guide impelmenters. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. | ||
| 86 | 161 | ||
| 87 | ```py | 162 | ```py |
| 88 | # Calculates length of the padded byte array. | 163 | # Calculates length of the padded byte array. |
| @@ -177,73 +252,13 @@ def decrypt(payload, conversation_key): | |||
| 177 | # 'hello world' == decrypt(payload, conversation_key) | 252 | # 'hello world' == decrypt(payload, conversation_key) |
| 178 | ``` | 253 | ``` |
| 179 | 254 | ||
| 180 | #### Encryption | 255 | ### Audit |
| 181 | |||
| 182 | 1. Calculate conversation key | ||
| 183 | - Execute ECDH (scalar multiplication) of public key B by private key A. | ||
| 184 | Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point. | ||
| 185 | - Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')` | ||
| 186 | - HKDF output will be `conversation_key` between two users | ||
| 187 | - It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)` | ||
| 188 | 2. Generate random 32-byte nonce | ||
| 189 | - Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) | ||
| 190 | - Don't generate nonce from message content | ||
| 191 | - Don't re-use the same nonce between messages: doing so would make them decryptable, | ||
| 192 | but won't leak long-term key | ||
| 193 | 3. Calculate message keys | ||
| 194 | - The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes | ||
| 195 | - Use HKDF-expand, with sha256, `OKM=conversation_key`, `info=nonce` and `L=76` | ||
| 196 | - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76) | ||
| 197 | 4. Add padding | ||
| 198 | - Content must be encoded from UTF-8 into byte array | ||
| 199 | - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes | ||
| 200 | - Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]` | ||
| 201 | - Padding algorithm is related to powers-of-two, with min padded msg size of 32 | ||
| 202 | - Plaintext length is encoded in big-endian as first 2 bytes of the padded blob | ||
| 203 | 5. Encrypt padded content | ||
| 204 | - Use ChaCha20, with key and nonce from step 3 | ||
| 205 | 6. Calculate MAC (message authentication code) with AAD | ||
| 206 | - AAD is used: instead of calculating MAC on ciphertext, | ||
| 207 | it's calculated over a concatenation of `nonce` and `ciphertext` | ||
| 208 | - Validate that AAD (nonce) is 32 bytes | ||
| 209 | 7. Base64-encode (with padding) params: `concat(version, nonce, ciphertext, mac)` | ||
| 210 | |||
| 211 | After encryption, it's necessary to sign it. Use NIP-01 to serialize the event, with result base64 assigned to event's `content`. Then, use NIP-01 to sign the event using schnorr signature scheme over secp256k1. | ||
| 212 | |||
| 213 | #### Decryption | ||
| 214 | |||
| 215 | Before decryption, it's necessary to validate the message's pubkey and signature. The public key must be a valid non-zero secp256k1 curve point, and signature must be valid secp256k1 schnorr signature. For exact validation rules, refer to BIP-340. | ||
| 216 | |||
| 217 | 1. Check if first payload's character is `#` | ||
| 218 | - `#` is an optional future-proof flag that means non-base64 encoding is used | ||
| 219 | - The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`, | ||
| 220 | an app must say the encryption version is not yet supported | ||
| 221 | 2. Decode base64 | ||
| 222 | - Base64 is decoded into `version, nonce, ciphertext, mac` | ||
| 223 | - If the version is unknown, the app, an app must say the encryption version is not yet supported | ||
| 224 | - Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars | ||
| 225 | - Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes | ||
| 226 | 3. Calculate conversation key | ||
| 227 | - See step 1 of Encryption | ||
| 228 | 4. Calculate message keys | ||
| 229 | - See step 3 of Encryption | ||
| 230 | 5. Calculate MAC (message authentication code) with AAD and compare | ||
| 231 | - Stop and throw an error if MAC doesn't match the decoded one from step 2 | ||
| 232 | - Use constant-time comparison algorithm | ||
| 233 | 6. Decrypt ciphertext | ||
| 234 | - Use ChaCha20 with key and nonce from step 3 | ||
| 235 | 7. Remove padding | ||
| 236 | - Read the first two BE bytes of plaintext that correspond to plaintext length | ||
| 237 | - Verify that the length of sliced plaintext matches the value of the two BE bytes | ||
| 238 | - Verify that calculated padding from encryption's step 3 matches the actual padding | ||
| 239 | |||
| 240 | ## Audit | ||
| 241 | 256 | ||
| 242 | The v2 of the standard has been subject to an audit by [Cure53](https://cure53.de) in December 2023. | 257 | The v2 of the standard was audited by [Cure53](https://cure53.de) in December 2023. |
| 243 | Check out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf) | 258 | Check out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf) |
| 244 | and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf). | 259 | and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf). |
| 245 | 260 | ||
| 246 | ## Tests and code | 261 | ### Tests and code |
| 247 | 262 | ||
| 248 | A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. | 263 | A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. |
| 249 | 264 | ||
| @@ -251,7 +266,7 @@ We publish extensive test vectors. Instead of having it in the document directly | |||
| 251 | 266 | ||
| 252 | 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json | 267 | 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json |
| 253 | 268 | ||
| 254 | Example of test vector from the file: | 269 | Example of a test vector from the file: |
| 255 | 270 | ||
| 256 | ```json | 271 | ```json |
| 257 | { | 272 | { |