upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Gleason <alex@alexgleason.me>2026-03-17 15:33:00 -0500
committerAlex Gleason <alex@alexgleason.me>2026-03-17 15:33:00 -0500
commitac2f6a6cf9c2368f1c6a87c1716751fdf7496707 (patch)
treea873560e18da7ab6a6c5750d0fb75eb0622d6111
parent3492eb1affa5e93e658224a81bb832d2b6090ecd (diff)
nip44: allow encryption of payloads larger than 65535 bytes
Extend the v2 padding format with a backwards-compatible sentinel: when the first 2 bytes of the length prefix are zero, the next 4 bytes encode the plaintext length as a big-endian u32. This raises the maximum from 65535 bytes to 2^32-1 bytes without requiring a version bump. Fixes from nostr-protocol/nips#1907: - Fix off-by-one: use >= 65536 (not > 65536) for the extended path, since u16 can only represent 0..65535 - Fix padding validation: use dynamic prefix_len (2 or 6) instead of hardcoded 2 in the unpad() size check - Fix len(d) typo in decode_payload (should be len(data)) - Remove upper-bound size checks in decode_payload that would reject large payloads - Add write_u32_be, read_uint16_be, read_uint32_be to function list - Add extended_prefix_threshold constant - Update size range comments for both small and large payload paths
-rw-r--r--44.md62
1 files changed, 40 insertions, 22 deletions
diff --git a/44.md b/44.md
index a7c13f1..4fe3cc1 100644
--- a/44.md
+++ b/44.md
@@ -84,10 +84,12 @@ NIP-44 version 2 has the following design characteristics:
84 - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..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 854. Add padding
86 - Content must be encoded from UTF-8 into byte array 86 - Content must be encoded from UTF-8 into byte array
87 - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes 87 - Validate plaintext length. Minimum is 1 byte, maximum is 4,294,967,295 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 bytes 88 - Padding algorithm is related to powers-of-two, with min padded msg size of 32 bytes
90 - Plaintext length is encoded in big-endian as first 2 bytes of the padded blob 89 - Plaintext length prefix is encoded in big-endian:
90 - If length is less than 65536: prefix is 2 bytes (`u16`), format is `[plaintext_length: u16][plaintext][zero_bytes]`
91 - If length is 65536 or greater: prefix is 6 bytes (2 zero bytes + `u32`), format is `[0x00, 0x00][plaintext_length: u32][plaintext][zero_bytes]`
92 - A zero value in the first 2 bytes signals the extended format; since valid plaintext is at least 1 byte, a u16 length of 0 is otherwise invalid
915. Encrypt padded content 935. Encrypt padded content
92 - Use ChaCha20, with key and nonce from step 3 94 - Use ChaCha20, with key and nonce from step 3
936. Calculate MAC (message authentication code) 956. Calculate MAC (message authentication code)
@@ -112,8 +114,8 @@ validation rules, refer to BIP-340.
1122. Decode base64 1142. Decode base64
113 - Base64 is decoded into `version, nonce, ciphertext, mac` 115 - Base64 is decoded into `version, nonce, ciphertext, mac`
114 - If the version is unknown, implementations must indicate that the encryption version is not supported 116 - 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 117 - Validate minimum length of base64 message to prevent DoS on base64 decoder: it must be at least 132 chars
116 - Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes 118 - Validate minimum length of decoded message to verify output of the decoder: it must be at least 99 bytes
1173. Calculate conversation key 1193. Calculate conversation key
118 - See step 1 of [encryption](#Encryption) 120 - See step 1 of [encryption](#Encryption)
1194. Calculate message keys 1214. Calculate message keys
@@ -124,8 +126,10 @@ validation rules, refer to BIP-340.
1246. Decrypt ciphertext 1266. Decrypt ciphertext
125 - Use ChaCha20 with key and nonce from step 3 127 - Use ChaCha20 with key and nonce from step 3
1267. Remove padding 1287. Remove padding
127 - Read the first two BE bytes of plaintext that correspond to plaintext length 129 - Read the first 2 bytes as a big-endian u16
128 - Verify that the length of sliced plaintext matches the value of the two BE bytes 130 - If zero, read the next 4 bytes as a big-endian u32 plaintext length (6-byte prefix total)
131 - Otherwise, use those 2 bytes as the u16 plaintext length (2-byte prefix total)
132 - Verify that the length of sliced plaintext matches the decoded length
129 - Verify that calculated padding from step 3 of the [encryption](#Encryption) process matches the actual padding 133 - Verify that calculated padding from step 3 of the [encryption](#Encryption) process matches the actual padding
130 134
131### Details 135### Details
@@ -149,7 +153,8 @@ validation rules, refer to BIP-340.
149 `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`. 153 `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`.
150- Constants `c`: 154- Constants `c`:
151 - `min_plaintext_size` is 1. 1 byte msg is padded to 32 bytes. 155 - `min_plaintext_size` is 1. 1 byte msg is padded to 32 bytes.
152 - `max_plaintext_size` is 65535 (64kB - 1). It is padded to 65536 bytes. 156 - `max_plaintext_size` is 4294967295 (2^32 - 1).
157 - `extended_prefix_threshold` is 65536. Lengths below this use a 2-byte u16 prefix; lengths at or above use a 6-byte prefix (2 zero bytes + u32).
153- Functions 158- Functions
154 - `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding) 159 - `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding)
155 - `concat` refers to byte array concatenation 160 - `concat` refers to byte array concatenation
@@ -157,6 +162,9 @@ validation rules, refer to BIP-340.
157 - `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back 162 - `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back
158 - `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array 163 - `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array
159 - `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array 164 - `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array
165 - `write_u32_be(number)` restricts number to values 0..4294967295 and encodes into Big-Endian uint32 byte array
166 - `read_uint16_be(bytes)` reads 2 bytes as a Big-Endian unsigned 16-bit integer
167 - `read_uint32_be(bytes)` reads 4 bytes as a Big-Endian unsigned 32-bit integer
160 - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros 168 - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros
161 - `floor(number)` and `log2(number)` are well-known mathematical methods 169 - `floor(number)` and `log2(number)` are well-known mathematical methods
162 170
@@ -181,35 +189,45 @@ def calc_padded_len(unpadded_len):
181# Converts unpadded plaintext to padded bytearray 189# Converts unpadded plaintext to padded bytearray
182def pad(plaintext): 190def pad(plaintext):
183 unpadded = utf8_encode(plaintext) 191 unpadded = utf8_encode(plaintext)
184 unpadded_len = len(plaintext) 192 unpadded_len = len(unpadded)
185 if (unpadded_len < c.min_plaintext_size or 193 if (unpadded_len < c.min_plaintext_size or
186 unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length') 194 unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length')
187 prefix = write_u16_be(unpadded_len) 195 if unpadded_len >= c.extended_prefix_threshold:
196 prefix = concat([0, 0], write_u32_be(unpadded_len)) # 6 bytes
197 else:
198 prefix = write_u16_be(unpadded_len) # 2 bytes
188 suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len) 199 suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len)
189 return concat(prefix, unpadded, suffix) 200 return concat(prefix, unpadded, suffix)
190 201
191# Converts padded bytearray to unpadded plaintext 202# Converts padded bytearray to unpadded plaintext
192def unpad(padded): 203def unpad(padded):
193 unpadded_len = read_uint16_be(padded[0:2]) 204 first_two = read_uint16_be(padded[0:2])
194 unpadded = padded[2:2+unpadded_len] 205 if first_two == 0:
206 unpadded_len = read_uint32_be(padded[2:6])
207 prefix_len = 6
208 else:
209 unpadded_len = first_two
210 prefix_len = 2
211 unpadded = padded[prefix_len:prefix_len+unpadded_len]
195 if (unpadded_len == 0 or 212 if (unpadded_len == 0 or
196 len(unpadded) != unpadded_len or 213 len(unpadded) != unpadded_len or
197 len(padded) != 2 + calc_padded_len(unpadded_len)): raise Exception('invalid padding') 214 len(padded) != prefix_len + calc_padded_len(unpadded_len)): raise Exception('invalid padding')
198 return utf8_decode(unpadded) 215 return utf8_decode(unpadded)
199 216
200# metadata: always 65b (version: 1b, nonce: 32b, max: 32b) 217# metadata: always 65b (version: 1b, nonce: 32b, mac: 32b)
201# plaintext: 1b to 0xffff 218# plaintext: 1b to 0xffffffff
202# padded plaintext: 32b to 0xffff 219# padded plaintext (small, <65536): 32b to 0xffff, with 2b prefix -> 34b to 0xffff+2
203# ciphertext: 32b+2 to 0xffff+2 220# padded plaintext (large, >=65536): 0x10000 to 0xffffffff, with 6b prefix -> 0x10006 to 0xffffffff+6
204# raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) 221# ciphertext: same as padded plaintext (chacha20 doesn't change length)
205# compressed payload (base64): 132b to 87472b 222# raw payload (small): 99 (65+34) to 65603 (65+0xffff+2)
223# raw payload (large): 65607 (65+0x10006) to 4294967362 (65+0xffffffff+6)
206def decode_payload(payload): 224def decode_payload(payload):
207 plen = len(payload) 225 plen = len(payload)
208 if plen == 0 or payload[0] == '#': raise Exception('unknown version') 226 if plen == 0 or payload[0] == '#': raise Exception('unknown version')
209 if plen < 132 or plen > 87472: raise Exception('invalid payload size') 227 if plen < 132: raise Exception('invalid payload size')
210 data = base64_decode(payload) 228 data = base64_decode(payload)
211 dlen = len(d) 229 dlen = len(data)
212 if dlen < 99 or dlen > 65603: raise Exception('invalid data size'); 230 if dlen < 99: raise Exception('invalid data size');
213 vers = data[0] 231 vers = data[0]
214 if vers != 2: raise Exception('unknown version ' + vers) 232 if vers != 2: raise Exception('unknown version ' + vers)
215 nonce = data[1:33] 233 nonce = data[1:33]