diff options
| author | Alex Gleason <alex@alexgleason.me> | 2026-03-17 16:30:09 -0500 |
|---|---|---|
| committer | Alex Gleason <alex@alexgleason.me> | 2026-03-17 16:30:09 -0500 |
| commit | 98fb2069515bf325faebe0d74a1ac739ed653d36 (patch) | |
| tree | b673f12ba57247828ac8644d788ca1832b9408f8 | |
| parent | ac2f6a6cf9c2368f1c6a87c1716751fdf7496707 (diff) | |
nip44: fix pseudocode bugs, comment arithmetic, add implementation guidance and test vectors
| -rw-r--r-- | 44.md | 45 |
1 files changed, 40 insertions, 5 deletions
| @@ -132,6 +132,17 @@ validation rules, refer to BIP-340. | |||
| 132 | - Verify that the length of sliced plaintext matches the decoded length | 132 | - Verify that the length of sliced plaintext matches the decoded length |
| 133 | - 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 |
| 134 | 134 | ||
| 135 | ### Implementation considerations | ||
| 136 | |||
| 137 | The theoretical maximum plaintext size is 2^32 - 1 bytes (~4 GB). Implementations SHOULD enforce | ||
| 138 | their own maximum payload size based on platform and resource constraints, rejecting oversized payloads | ||
| 139 | early in `decode_payload` (before base64 decoding) to prevent denial-of-service. Decryption may require | ||
| 140 | several times the payload size in working memory due to base64 decoding, byte array slicing, and | ||
| 141 | padding operations. For reference, JVM-based systems are limited to ~2 GB contiguous arrays, and mobile | ||
| 142 | devices may have significantly less available memory. Note that `calc_padded_len` can return values up | ||
| 143 | to 2^32, which exceeds the range of unsigned 32-bit integers; implementations must use 64-bit (or | ||
| 144 | larger) arithmetic for padding calculations. | ||
| 145 | |||
| 135 | ### Details | 146 | ### Details |
| 136 | 147 | ||
| 137 | - Cryptographic methods | 148 | - Cryptographic methods |
| @@ -184,7 +195,7 @@ def calc_padded_len(unpadded_len): | |||
| 184 | if unpadded_len <= 32: | 195 | if unpadded_len <= 32: |
| 185 | return 32 | 196 | return 32 |
| 186 | else: | 197 | else: |
| 187 | return chunk * (floor((len - 1) / chunk) + 1) | 198 | return chunk * (floor((unpadded_len - 1) / chunk) + 1) |
| 188 | 199 | ||
| 189 | # Converts unpadded plaintext to padded bytearray | 200 | # Converts unpadded plaintext to padded bytearray |
| 190 | def pad(plaintext): | 201 | def pad(plaintext): |
| @@ -216,11 +227,11 @@ def unpad(padded): | |||
| 216 | 227 | ||
| 217 | # metadata: always 65b (version: 1b, nonce: 32b, mac: 32b) | 228 | # metadata: always 65b (version: 1b, nonce: 32b, mac: 32b) |
| 218 | # plaintext: 1b to 0xffffffff | 229 | # plaintext: 1b to 0xffffffff |
| 219 | # padded plaintext (small, <65536): 32b to 0xffff, with 2b prefix -> 34b to 0xffff+2 | 230 | # padded plaintext (small, <65536): 32b to 0x10000, with 2b prefix -> 34b to 0x10000+2 |
| 220 | # padded plaintext (large, >=65536): 0x10000 to 0xffffffff, with 6b prefix -> 0x10006 to 0xffffffff+6 | 231 | # padded plaintext (large, >=65536): 0x10000 to 0x100000000, with 6b prefix -> 0x10006 to 0x100000000+6 |
| 221 | # ciphertext: same as padded plaintext (chacha20 doesn't change length) | 232 | # ciphertext: same as padded plaintext (chacha20 doesn't change length) |
| 222 | # raw payload (small): 99 (65+34) to 65603 (65+0xffff+2) | 233 | # raw payload (small): 99 (65+34) to 65603 (65+0x10000+2) |
| 223 | # raw payload (large): 65607 (65+0x10006) to 4294967362 (65+0xffffffff+6) | 234 | # raw payload (large): 65607 (65+0x10006) to 4294967367 (65+0x100000000+6) |
| 224 | def decode_payload(payload): | 235 | def decode_payload(payload): |
| 225 | plen = len(payload) | 236 | plen = len(payload) |
| 226 | if plen == 0 or payload[0] == '#': raise Exception('unknown version') | 237 | if plen == 0 or payload[0] == '#': raise Exception('unknown version') |
| @@ -313,3 +324,27 @@ The file also contains intermediate values. A quick guidance with regards to its | |||
| 313 | - `invalid.encrypt_msg_lengths` | 324 | - `invalid.encrypt_msg_lengths` |
| 314 | - `invalid.get_conversation_key`: calculating conversation_key must throw an error | 325 | - `invalid.get_conversation_key`: calculating conversation_key must throw an error |
| 315 | - `invalid.decrypt`: decrypting message content must throw an error | 326 | - `invalid.decrypt`: decrypting message content must throw an error |
| 327 | |||
| 328 | #### Extended length prefix test vectors | ||
| 329 | |||
| 330 | The following test vectors exercise the boundary between the 2-byte u16 prefix and the 6-byte | ||
| 331 | extended prefix. Since the payloads are too large to include inline, SHA-256 checksums of the | ||
| 332 | plaintext and base64-encoded payload are provided (following the `encrypt_decrypt_long_msg` pattern). | ||
| 333 | |||
| 334 | All vectors use the same `conversation_key` and `nonce` as above. Plaintext is the byte `0x61` | ||
| 335 | (`'a'`) repeated to the specified length. | ||
| 336 | |||
| 337 | ``` | ||
| 338 | conversation_key: c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d | ||
| 339 | nonce: 0000000000000000000000000000000000000000000000000000000000000001 | ||
| 340 | ``` | ||
| 341 | |||
| 342 | | plaintext_len | prefix | padded_len | plaintext_sha256 | payload_sha256 | | ||
| 343 | |---|---|---|---|---| | ||
| 344 | | 65535 | u16 (2 bytes) | 65536 | `6e1bebca6a8229364a162a72ef064826c4cd7457bf54f190ef782bd9deff3e42` | `6d8c2810d1e870fbaa1f0a0937126cca837a15f9260e27060c331d70a3c0bc84` | | ||
| 345 | | 65536 | extended (6 bytes) | 65536 | `bf718b6f653bebc184e1479f1935b8da974d701b893afcf49e701f3e2f9f9c5a` | `b7b4edb36ba92e267d322d56d9aebc22e7fa96ff52e3c12adc07f07a43cbc616` | | ||
| 346 | | 65537 | extended (6 bytes) | 81920 | `008ffc88d3c96a9f307524eb361e47c5222a887fc45fa0c1fb8d429c5c23b430` | `eeb7c7c5373894ea2c1547cfd3ccb15d5a0b2d619da852e5c79df792dcc9e435` | | ||
| 347 | |||
| 348 | Note that 65535 and 65536 both have a `padded_len` of 65536, but the total padded-with-prefix | ||
| 349 | sizes differ: 65538 (2 + 65536) vs 65542 (6 + 65536). The jump to 65537 triggers the next | ||
| 350 | padding bucket at 81920. | ||