upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/44.md
diff options
context:
space:
mode:
Diffstat (limited to '44.md')
-rw-r--r--44.md295
1 files changed, 295 insertions, 0 deletions
diff --git a/44.md b/44.md
new file mode 100644
index 0000000..f3071ea
--- /dev/null
+++ b/44.md
@@ -0,0 +1,295 @@
1NIP-44
2=====
3
4Encrypted Payloads (Versioned)
5------------------------------
6
7`optional`
8
9The NIP introduces a new data format for keypair-based encryption. This NIP is versioned
10to allow multiple algorithm choices to exist simultaneously. This format may be used for
11many things, but MUST be used in the context of a signed event as described in NIP 01.
12
13*Note*: this format DOES NOT define any `kind`s related to a new direct messaging standard,
14only the encryption required to define one. It SHOULD NOT be used as a drop-in replacement
15for NIP 04 payloads.
16
17## Versions
18
19Currently defined encryption algorithms:
20
21- `0x00` - Reserved
22- `0x01` - Deprecated and undefined
23- `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64
24
25## Limitations
26
27Every nostr user has their own public key, which solves key distribution problems present
28in other solutions. However, nostr's relay-based architecture makes it difficult to implement
29more robust private messaging protocols with things like metadata hiding, forward secrecy,
30and post compromise secrecy.
31
32The goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed
33event. When applying this NIP to any use case, it's important to keep in mind your users' threat
34model and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE
35messaging software and limit use of nostr to exchanging contacts.
36
37On its own, messages sent using this scheme have a number of important shortcomings:
38
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
47
48Lack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking
49relays to delete stored messages after a certain duration has elapsed.
50
51## Version 2
52
53NIP-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
701. 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)`
762. 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
813. 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, `PRK=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)
854. 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
915. Encrypt padded content
92 - Use ChaCha20, with key and nonce from step 3
936. 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
977. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)`
98
99Encrypted payloads MUST be included in an event's payload, hashed, and signed as defined in NIP 01, using schnorr
100signature scheme over secp256k1.
101
102### Decryption
103
104Before decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be
105a valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact
106validation rules, refer to BIP-340.
107
1081. Check if first payload's character is `#`
109 - `#` is an optional future-proof flag that means non-base64 encoding is used
110 - The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`,
111 implementations MUST indicate that the encryption version is not yet supported
1122. Decode base64
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
1173. Calculate conversation key
118 - See step 1 of [encryption](#Encryption)
1194. Calculate message keys
120 - See step 3 of [encryption](#Encryption)
1215. 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
1246. Decrypt ciphertext
125 - Use ChaCha20 with key and nonce from step 3
1267. 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
130
131### Details
132
133- Cryptographic methods
134 - `secure_random_bytes(length)` fetches randomness from CSPRNG.
135 - `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869)
136 with SHA256 hash function comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)`.
137 - `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439) with
138 starting counter set to 0.
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]`.
145- Operators
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
147 `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`.
148- Constants `c`:
149 - `min_plaintext_size` is 1. 1b msg is padded to 32b.
150 - `max_plaintext_size` is 65535 (64kb - 1). It is padded to 65536.
151- Functions
152 - `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding)
153 - `concat` refers to byte array concatenation
154 - `is_equal_ct(a, b)` is constant-time equality check of 2 byte arrays
155 - `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back
156 - `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array
157 - `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array
158 - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros
159 - `floor(number)` and `log2(number)` are well-known mathematical methods
160
161### Implementation pseudocode
162
163The following is a collection of python-like pseudocode functions which implement the above primitives,
164intended to guide implementers. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.
165
166```py
167# Calculates length of the padded byte array.
168def calc_padded_len(unpadded_len):
169 next_power = 1 << (floor(log2(unpadded_len - 1))) + 1
170 if next_power <= 256:
171 chunk = 32
172 else:
173 chunk = next_power / 8
174 if unpadded_len <= 32:
175 return 32
176 else:
177 return chunk * (floor((len - 1) / chunk) + 1)
178
179# Converts unpadded plaintext to padded bytearray
180def pad(plaintext):
181 unpadded = utf8_encode(plaintext)
182 unpadded_len = len(plaintext)
183 if (unpadded_len < c.min_plaintext_size or
184 unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length')
185 prefix = write_u16_be(unpadded_len)
186 suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len)
187 return concat(prefix, unpadded, suffix)
188
189# Converts padded bytearray to unpadded plaintext
190def unpad(padded):
191 unpadded_len = read_uint16_be(padded[0:2])
192 unpadded = padded[2:2+unpadded_len]
193 if (unpadded_len == 0 or
194 len(unpadded) != unpadded_len or
195 len(padded) != 2 + calc_padded_len(unpadded_len)): raise Exception('invalid padding')
196 return utf8_decode(unpadded)
197
198# metadata: always 65b (version: 1b, nonce: 32b, max: 32b)
199# plaintext: 1b to 0xffff
200# padded plaintext: 32b to 0xffff
201# ciphertext: 32b+2 to 0xffff+2
202# raw payload: 99 (65+32+2) to 65603 (65+0xffff+2)
203# compressed payload (base64): 132b to 87472b
204def decode_payload(payload):
205 plen = len(payload)
206 if plen == 0 or payload[0] == '#': raise Exception('unknown version')
207 if plen < 132 or plen > 87472: raise Exception('invalid payload size')
208 data = base64_decode(payload)
209 dlen = len(d)
210 if dlen < 99 or dlen > 65603: raise Exception('invalid data size');
211 vers = data[0]
212 if vers != 2: raise Exception('unknown version ' + vers)
213 nonce = data[1:33]
214 ciphertext = data[33:dlen - 32]
215 mac = data[dlen - 32:dlen]
216 return (nonce, ciphertext, mac)
217
218def hmac_aad(key, message, aad):
219 if len(aad) != 32: raise Exception('AAD associated data must be 32 bytes');
220 return hmac(sha256, key, concat(aad, message));
221
222# Calculates long-term key between users A and B: `get_key(Apriv, Bpub) == get_key(Bpriv, Apub)`
223def get_conversation_key(private_key_a, public_key_b):
224 shared_x = secp256k1_ecdh(private_key_a, public_key_b)
225 return hkdf_extract(IKM=shared_x, salt=utf8_encode('nip44-v2'))
226
227# Calculates unique per-message key
228def get_message_keys(conversation_key, nonce):
229 if len(conversation_key) != 32: raise Exception('invalid conversation_key length')
230 if len(nonce) != 32: raise Exception('invalid nonce length')
231 keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76)
232 chacha_key = keys[0:32]
233 chacha_nonce = keys[32:44]
234 hmac_key = keys[44:76]
235 return (chacha_key, chacha_nonce, hmac_key)
236
237def encrypt(plaintext, conversation_key, nonce):
238 (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)
239 padded = pad(plaintext)
240 ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded)
241 mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)
242 return base64_encode(concat(write_u8(2), nonce, ciphertext, mac))
243
244def decrypt(payload, conversation_key):
245 (nonce, ciphertext, mac) = decode_payload(payload)
246 (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)
247 calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)
248 if not is_equal_ct(calculated_mac, mac): raise Exception('invalid MAC')
249 padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext)
250 return unpad(padded_plaintext)
251
252# Usage:
253# conversation_key = get_conversation_key(sender_privkey, recipient_pubkey)
254# nonce = secure_random_bytes(32)
255# payload = encrypt('hello world', conversation_key, nonce)
256# 'hello world' == decrypt(payload, conversation_key)
257```
258
259### Audit
260
261The v2 of the standard was audited by [Cure53](https://cure53.de) in December 2023.
262Check out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf)
263and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf).
264
265### Tests and code
266
267A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.
268
269We publish extensive test vectors. Instead of having it in the document directly, a sha256 checksum of vectors is provided:
270
271 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json
272
273Example of a test vector from the file:
274
275```json
276{
277 "sec1": "0000000000000000000000000000000000000000000000000000000000000001",
278 "sec2": "0000000000000000000000000000000000000000000000000000000000000002",
279 "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",
280 "nonce": "0000000000000000000000000000000000000000000000000000000000000001",
281 "plaintext": "a",
282 "payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
283}
284```
285
286The file also contains intermediate values. A quick guidance with regards to its usage:
287
288- `valid.get_conversation_key`: calculate conversation_key from secret key sec1 and public key pub2
289- `valid.get_message_keys`: calculate chacha_key, chacha_nonce, hmac_key from conversation_key and nonce
290- `valid.calc_padded_len`: take unpadded length (first value), calculate padded length (second value)
291- `valid.encrypt_decrypt`: emulate real conversation. Calculate pub2 from sec2, verify conversation_key from (sec1, pub2), encrypt, verify payload, then calculate pub1 from sec1, verify conversation_key from (sec2, pub1), decrypt, verify plaintext.
292- `valid.encrypt_decrypt_long_msg`: same as previous step, but instead of a full plaintext and payload, their checksum is provided.
293- `invalid.encrypt_msg_lengths`
294- `invalid.get_conversation_key`: calculating conversation_key must throw an error
295- `invalid.decrypt`: decrypting message content must throw an error