diff options
| author | hodlbod <jstaab@protonmail.com> | 2023-12-22 06:56:51 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-22 06:56:51 -0800 |
| commit | ffc32c43e6b08ac54d63b21c8887f0f073c716ba (patch) | |
| tree | 1a38c6bcb3d53d27101ea93de39edaea6badb53d /44.md | |
| parent | 5ed4232584f3ab34192291daf985742248fb14ea (diff) | |
| parent | 2b78cc9304f775b8391f62b7fe61e99a3fdc905b (diff) | |
Merge pull request #939 from coracle-social/nip44-tweaks
Clean up NIP 44
Diffstat (limited to '44.md')
| -rw-r--r-- | 44.md | 241 |
1 files changed, 130 insertions, 111 deletions
| @@ -1,74 +1,150 @@ | |||
| 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 | *Note*: this format DOES NOT define any `kind`s related to a new direct messaging standard, |
| 11 | distribution problems present in other solutions. The goal of this NIP is to have a | 14 | only the encryption required to define one. It SHOULD NOT be used as a drop-in replacement |
| 12 | simple way to send messages between nostr accounts that cannot be read by everyone. | 15 | for NIP 04 payloads. |
| 13 | 16 | ||
| 14 | The scheme has a number of important shortcomings: | 17 | ## Versions |
| 15 | 18 | ||
| 16 | - No deniability: it is possible to prove the event was signed by a particular key | 19 | 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 | 20 | ||
| 25 | Lack of forward secrecy is partially mitigated by these two factors: | 21 | - `0x00` - Reserved |
| 26 | 1. the messages should only be stored on relays, specified by the user, instead of a set of all public relays. | 22 | - `0x01` - Deprecated and undefined |
| 27 | 2. the relays are supposed to regularly delete older messages. | 23 | - `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64 |
| 28 | 24 | ||
| 29 | For risky situations, users should chat in specialized E2EE messaging software and limit use of nostr to exchanging contacts. | 25 | ## Limitations |
| 30 | 26 | ||
| 31 | ## Dependence on NIP-01 | 27 | Every nostr user has their own public key, which solves key distribution problems present |
| 28 | in other solutions. However, nostr's relay-based architecture makes it difficult to implement | ||
| 29 | more robust private messaging protocols with things like metadata hiding, forward secrecy, | ||
| 30 | and post compromise secrecy. | ||
| 32 | 31 | ||
| 33 | It's not enough to use NIP-44 for encryption: the output must also be signed. | 32 | The goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed |
| 33 | event. When applying this NIP to any use case, it's important to keep in mind your users' threat | ||
| 34 | model and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE | ||
| 35 | messaging software and limit use of nostr to exchanging contacts. | ||
| 34 | 36 | ||
| 35 | In nostr case, the payload is serialized and signed as per NIP-01 rules. | 37 | On its own, messages sent using this scheme have a number of important shortcomings: |
| 36 | 38 | ||
| 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. | 39 | - No deniability: it is possible to prove an event was signed by a particular key |
| 40 | - No forward secrecy: when a key is compromised, it is possible to decrypt all previous conversations | ||
| 41 | - No post-compromise security: when a key is compromised, it is possible to decrypt all future conversations | ||
| 42 | - No post-quantum security: a powerful quantum computer would be able to decrypt the messages | ||
| 43 | - IP address leak: user IP may be seen by relays and all intermediaries between user and relay | ||
| 44 | - Date leak: `created_at` is public, since it is a part of NIP 01 event | ||
| 45 | - Limited message size leak: padding only partially obscures true message length | ||
| 46 | - No attachments: they are not supported | ||
| 38 | 47 | ||
| 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. | 48 | Lack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking |
| 49 | relays to delete stored messages after a certain duration has elapsed. | ||
| 40 | 50 | ||
| 41 | ## Versions | 51 | ## Version 2 |
| 42 | 52 | ||
| 43 | Currently defined encryption algorithms: | 53 | NIP-44 version 2 has the following design characteristics: |
| 54 | |||
| 55 | - Payloads are authenticated using a MAC before signing rather than afterwards because events are assumed | ||
| 56 | to be signed as specified in NIP-01. The outer signature serves to authenticate the full payload, and MUST | ||
| 57 | be validated before decrypting. | ||
| 58 | - ChaCha is used instead of AES because it's faster and has | ||
| 59 | [better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/). | ||
| 60 | - ChaCha is used instead of XChaCha because XChaCha has not been standardized. Also, xChaCha's improved collision | ||
| 61 | resistance of nonces isn't necessary since every message has a new (key, nonce) pair. | ||
| 62 | - HMAC-SHA256 is used instead of Poly1305 because polynomial MACs are much easier to forge. | ||
| 63 | - SHA256 is used instead of SHA3 or BLAKE because it is already used in nostr. Also BLAKE's speed advantage | ||
| 64 | is smaller in non-parallel environments. | ||
| 65 | - A custom padding scheme is used instead of padmé because it provides better leakage reduction for small messages. | ||
| 66 | - Base64 encoding is used instead of another compression algorithm because it is widely available, and is already used in nostr. | ||
| 67 | |||
| 68 | ### Encryption | ||
| 69 | |||
| 70 | 1. Calculate a conversation key | ||
| 71 | - Execute ECDH (scalar multiplication) of public key B by private key A | ||
| 72 | Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point | ||
| 73 | - Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')` | ||
| 74 | - HKDF output will be a `conversation_key` between two users. | ||
| 75 | - It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)` | ||
| 76 | 2. Generate a random 32-byte nonce | ||
| 77 | - Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) | ||
| 78 | - Don't generate a nonce from message content | ||
| 79 | - Don't re-use the same nonce between messages: doing so would make them decryptable, | ||
| 80 | but won't leak the long-term key | ||
| 81 | 3. Calculate message keys | ||
| 82 | - The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes long | ||
| 83 | - Use HKDF-expand, with sha256, `OKM=conversation_key`, `info=nonce` and `L=76` | ||
| 84 | - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76) | ||
| 85 | 4. Add padding | ||
| 86 | - Content must be encoded from UTF-8 into byte array | ||
| 87 | - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes | ||
| 88 | - Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]` | ||
| 89 | - Padding algorithm is related to powers-of-two, with min padded msg size of 32 | ||
| 90 | - Plaintext length is encoded in big-endian as first 2 bytes of the padded blob | ||
| 91 | 5. Encrypt padded content | ||
| 92 | - Use ChaCha20, with key and nonce from step 3 | ||
| 93 | 6. Calculate MAC (message authentication code) | ||
| 94 | - AAD (additional authenticated data) is used - instead of calculating MAC on ciphertext, | ||
| 95 | it's calculated over a concatenation of `nonce` and `ciphertext` | ||
| 96 | - Validate that AAD (nonce) is 32 bytes | ||
| 97 | 7. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)` | ||
| 44 | 98 | ||
| 45 | - `0x00` - Reserved | 99 | 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 | 100 | signature scheme over secp256k1. |
| 47 | - `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64 | ||
| 48 | 101 | ||
| 49 | ## Version 2 | 102 | ### Decryption |
| 50 | 103 | ||
| 51 | The algorithm choices are justified in a following way: | 104 | Before decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be |
| 105 | a valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact | ||
| 106 | validation rules, refer to BIP-340. | ||
| 52 | 107 | ||
| 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. | 108 | 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/) | 109 | - `#` 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. | 110 | - 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 | 111 | 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 | 112 | 2. Decode base64 |
| 58 | - Base64 encoding instead of an other compression algorithm: it is widely available, and is already used in nostr | 113 | - Base64 is decoded into `version, nonce, ciphertext, mac` |
| 114 | - If the version is unknown, implementations must indicate that the encryption version is not supported | ||
| 115 | - Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars | ||
| 116 | - Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes | ||
| 117 | 3. Calculate conversation key | ||
| 118 | - See step 1 of (encryption)[#Encryption] | ||
| 119 | 4. Calculate message keys | ||
| 120 | - See step 3 of (encryption)[#Encryption] | ||
| 121 | 5. Calculate MAC (message authentication code) with AAD and compare | ||
| 122 | - Stop and throw an error if MAC doesn't match the decoded one from step 2 | ||
| 123 | - Use constant-time comparison algorithm | ||
| 124 | 6. Decrypt ciphertext | ||
| 125 | - Use ChaCha20 with key and nonce from step 3 | ||
| 126 | 7. Remove padding | ||
| 127 | - Read the first two BE bytes of plaintext that correspond to plaintext length | ||
| 128 | - Verify that the length of sliced plaintext matches the value of the two BE bytes | ||
| 129 | - Verify that calculated padding from step 3 of the (encryption)[#Encryption] process matches the actual padding | ||
| 59 | 130 | ||
| 60 | ### Functions and operations | 131 | ### Details |
| 61 | 132 | ||
| 62 | - Cryptographic methods | 133 | - Cryptographic methods |
| 63 | - `secure_random_bytes(length)` fetches randomness from CSPRNG | 134 | - `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, | 135 | - `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)` | 136 | 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 | 137 | - `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) | 138 | 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]` | 139 | - `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104). |
| 140 | - `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a ⋅ B`), defined in | ||
| 141 | [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki). | ||
| 142 | The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method | ||
| 143 | `bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid, | ||
| 144 | on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]`. | ||
| 69 | - Operators | 145 | - Operators |
| 70 | - `x[i:j]`, where `x` is a byte array and `i, j <= 0`, | 146 | - `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` | 147 | `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`. |
| 72 | - Constants `c`: | 148 | - Constants `c`: |
| 73 | - `min_plaintext_size` is 1. 1b msg is padded to 32b. | 149 | - `min_plaintext_size` is 1. 1b msg is padded to 32b. |
| 74 | - `max_plaintext_size` is 65535 (64kb - 1). It is padded to 65536. | 150 | - `max_plaintext_size` is 65535 (64kb - 1). It is padded to 65536. |
| @@ -82,7 +158,10 @@ The algorithm choices are justified in a following way: | |||
| 82 | - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros | 158 | - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros |
| 83 | - `floor(number)` and `log2(number)` are well-known mathematical methods | 159 | - `floor(number)` and `log2(number)` are well-known mathematical methods |
| 84 | 160 | ||
| 85 | User-defined functions: | 161 | ### Implementation pseudocode |
| 162 | |||
| 163 | The following is a collection of python-like pseudocode functions which implement the above primitives, | ||
| 164 | intended to guide impelmenters. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. | ||
| 86 | 165 | ||
| 87 | ```py | 166 | ```py |
| 88 | # Calculates length of the padded byte array. | 167 | # Calculates length of the padded byte array. |
| @@ -177,73 +256,13 @@ def decrypt(payload, conversation_key): | |||
| 177 | # 'hello world' == decrypt(payload, conversation_key) | 256 | # 'hello world' == decrypt(payload, conversation_key) |
| 178 | ``` | 257 | ``` |
| 179 | 258 | ||
| 180 | #### Encryption | 259 | ### 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 | 260 | ||
| 242 | The v2 of the standard has been subject to an audit by [Cure53](https://cure53.de) in December 2023. | 261 | 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) | 262 | 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). | 263 | and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf). |
| 245 | 264 | ||
| 246 | ## Tests and code | 265 | ### Tests and code |
| 247 | 266 | ||
| 248 | A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. | 267 | A collection of implementations in different languages is available at https://github.com/paulmillr/nip44. |
| 249 | 268 | ||
| @@ -251,7 +270,7 @@ We publish extensive test vectors. Instead of having it in the document directly | |||
| 251 | 270 | ||
| 252 | 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json | 271 | 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json |
| 253 | 272 | ||
| 254 | Example of test vector from the file: | 273 | Example of a test vector from the file: |
| 255 | 274 | ||
| 256 | ```json | 275 | ```json |
| 257 | { | 276 | { |