1NIP-442======34Encrypted Payloads (Versioned)5------------------------------67`optional`89The NIP introduces a new data format for keypair-based encryption. This NIP is versioned10to allow multiple algorithm choices to exist simultaneously. This format may be used for11many things, but MUST be used in the context of a signed event as described in NIP-01.1213*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 replacement15for NIP-04 payloads.1617## Versions1819Currently defined encryption algorithms:2021- `0x00` - Reserved22- `0x01` - Deprecated and undefined23- `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base642425## Limitations2627Every nostr user has their own public key, which solves key distribution problems present28in other solutions. However, nostr's relay-based architecture makes it difficult to implement29more robust private messaging protocols with things like metadata hiding, forward secrecy,30and post compromise secrecy.3132The goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed33event. When applying this NIP to any use case, it's important to keep in mind your users' threat34model and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE35messaging software and limit use of nostr to exchanging contacts.3637On its own, messages sent using this scheme have a number of important shortcomings:3839- No deniability: it is possible to prove an event was signed by a particular key40- No forward secrecy: when a key is compromised, it is possible to decrypt all previous conversations41- No post-compromise security: when a key is compromised, it is possible to decrypt all future conversations42- No post-quantum security: a powerful quantum computer would be able to decrypt the messages43- IP address leak: user IP may be seen by relays and all intermediaries between user and relay44- Date leak: `created_at` is public, since it is a part of NIP-01 event45- Limited message size leak: padding only partially obscures true message length46- No attachments: they are not supported4748Lack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking49relays to delete stored messages after a certain duration has elapsed.5051## Version 25253NIP-44 version 2 has the following design characteristics:5455- Payloads are authenticated using a MAC before signing rather than afterwards because events are assumed56 to be signed as specified in NIP-01. The outer signature serves to authenticate the full payload, and MUST57 be validated before decrypting.58- ChaCha is used instead of AES because it's faster and has59 [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 collision61 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 advantage64 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 encoding algorithm because it is widely available, and is already used in nostr.6768### Encryption69701. Calculate a conversation key71 - Execute ECDH (scalar multiplication) of public key B by private key A72 Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point73 - 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 nonce77 - Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator)78 - Don't generate a nonce from message content79 - Don't re-use the same nonce between messages: doing so would make them decryptable,80 but won't leak the long-term key813. Calculate message keys82 - The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes long83 - 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 padding86 - Content must be encoded from UTF-8 into byte array87 - Validate plaintext length. Minimum is 1 byte, maximum is 4,294,967,295 bytes88 - Padding algorithm is related to powers-of-two, with min padded msg size of 32 bytes89 - 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 invalid935. Encrypt padded content94 - Use ChaCha20, with key and nonce from step 3956. Calculate MAC (message authentication code)96 - AAD (additional authenticated data) is used - instead of calculating MAC on ciphertext,97 it's calculated over a concatenation of `nonce` and `ciphertext`98 - Validate that AAD (nonce) is 32 bytes997. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)`100101Encrypted payloads MUST be included in an event's payload, hashed, and signed as defined in NIP 01, using schnorr102signature scheme over secp256k1.103104### Decryption105106Before decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be107a valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact108validation rules, refer to BIP-340.1091101. Check if first payload's character is `#`111 - `#` is an optional future-proof flag that means non-base64 encoding is used112 - The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`,113 implementations MUST indicate that the encryption version is not yet supported1142. Decode base64115 - Base64 is decoded into `version, nonce, ciphertext, mac`116 - If the version is unknown, implementations must indicate that the encryption version is not supported117 - Validate minimum length of base64 message to prevent DoS on base64 decoder: it must be at least 132 chars118 - Validate minimum length of decoded message to verify output of the decoder: it must be at least 99 bytes1193. Calculate conversation key120 - See step 1 of [encryption](#Encryption)1214. Calculate message keys122 - See step 3 of [encryption](#Encryption)1235. Calculate MAC (message authentication code) with AAD and compare124 - Stop and throw an error if MAC doesn't match the decoded one from step 2125 - Use constant-time comparison algorithm1266. Decrypt ciphertext127 - Use ChaCha20 with key and nonce from step 31287. Remove padding129 - Read the first 2 bytes as a big-endian u16130 - 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 length133 - Verify that calculated padding from step 3 of the [encryption](#Encryption) process matches the actual padding134135### Implementation considerations136137The theoretical maximum plaintext size is 2^32 - 1 bytes (~4 GB). Implementations SHOULD enforce138their own maximum payload size based on platform and resource constraints, rejecting oversized payloads139early in `decode_payload` (before base64 decoding) to prevent denial-of-service. Decryption may require140several times the payload size in working memory due to base64 decoding, byte array slicing, and141padding operations. For reference, JVM-based systems are limited to ~2 GB contiguous arrays, and mobile142devices may have significantly less available memory. Note that `calc_padded_len` can return values up143to 2^32, which exceeds the range of unsigned 32-bit integers; implementations must use 64-bit (or144larger) arithmetic for padding calculations.145146### Details147148- Cryptographic methods149 - `secure_random_bytes(length)` fetches randomness from CSPRNG.150 - `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869)151 with SHA256 hash function comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)`.152 - `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439) with153 starting counter set to 0.154 - `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104).155 - `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a ⋅ B`), defined in156 [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki).157 The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method158 `bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid,159 on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]`.160 NIP44 doesn't do hashing of the output: keep this in mind, because some libraries hash it using sha256.161 As an example, in libsecp256k1, unhashed version is available in `secp256k1_ec_pubkey_tweak_mul`162- Operators163 - `x[i:j]`, where `x` is a byte array and `i, j <= 0` returns a `(j - i)`-byte array with a copy of the164 `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`.165- Constants `c`:166 - `min_plaintext_size` is 1. 1 byte msg is padded to 32 bytes.167 - `max_plaintext_size` is 4294967295 (2^32 - 1).168 - `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).169- Functions170 - `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding)171 - `concat` refers to byte array concatenation172 - `is_equal_ct(a, b)` is constant-time equality check of 2 byte arrays173 - `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back174 - `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array175 - `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array176 - `write_u32_be(number)` restricts number to values 0..4294967295 and encodes into Big-Endian uint32 byte array177 - `read_uint16_be(bytes)` reads 2 bytes as a Big-Endian unsigned 16-bit integer178 - `read_uint32_be(bytes)` reads 4 bytes as a Big-Endian unsigned 32-bit integer179 - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros180 - `floor(number)` and `log2(number)` are well-known mathematical methods181182### Implementation pseudocode183184The following is a collection of python-like pseudocode functions which implement the above primitives,185intended to guide implementers. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.186187```py188# Calculates length of the padded byte array.189def calc_padded_len(unpadded_len):190 next_power = 1 << (floor(log2(unpadded_len - 1))) + 1191 if next_power <= 256:192 chunk = 32193 else:194 chunk = next_power / 8195 if unpadded_len <= 32:196 return 32197 else:198 return chunk * (floor((unpadded_len - 1) / chunk) + 1)199200# Converts unpadded plaintext to padded bytearray201def pad(plaintext):202 unpadded = utf8_encode(plaintext)203 unpadded_len = len(unpadded)204 if (unpadded_len < c.min_plaintext_size or205 unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length')206 if unpadded_len >= c.extended_prefix_threshold:207 prefix = concat([0, 0], write_u32_be(unpadded_len)) # 6 bytes208 else:209 prefix = write_u16_be(unpadded_len) # 2 bytes210 suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len)211 return concat(prefix, unpadded, suffix)212213# Converts padded bytearray to unpadded plaintext214def unpad(padded):215 first_two = read_uint16_be(padded[0:2])216 if first_two == 0:217 unpadded_len = read_uint32_be(padded[2:6])218 if unpadded_len < c.extended_prefix_threshold: raise Exception('invalid padding')219 prefix_len = 6220 else:221 unpadded_len = first_two222 prefix_len = 2223 unpadded = padded[prefix_len:prefix_len+unpadded_len]224 if (unpadded_len == 0 or225 len(unpadded) != unpadded_len or226 len(padded) != prefix_len + calc_padded_len(unpadded_len)): raise Exception('invalid padding')227 return utf8_decode(unpadded)228229# metadata: always 65b (version: 1b, nonce: 32b, mac: 32b)230# plaintext: 1b to 0xffffffff231# padded plaintext (small, <65536): 32b to 0x10000, with 2b prefix -> 34b to 0x10000+2232# padded plaintext (large, >=65536): 0x10000 to 0x100000000, with 6b prefix -> 0x10006 to 0x100000000+6233# ciphertext: same as padded plaintext (chacha20 doesn't change length)234# raw payload (small): 99 (65+34) to 65603 (65+0x10000+2)235# raw payload (large): 65607 (65+0x10006) to 4294967367 (65+0x100000000+6)236def decode_payload(payload):237 plen = len(payload)238 if plen == 0 or payload[0] == '#': raise Exception('unknown version')239 if plen < 132: raise Exception('invalid payload size')240 data = base64_decode(payload)241 dlen = len(data)242 if dlen < 99: raise Exception('invalid data size');243 vers = data[0]244 if vers != 2: raise Exception('unknown version ' + vers)245 nonce = data[1:33]246 ciphertext = data[33:dlen - 32]247 mac = data[dlen - 32:dlen]248 return (nonce, ciphertext, mac)249250def hmac_aad(key, message, aad):251 if len(aad) != 32: raise Exception('AAD associated data must be 32 bytes');252 return hmac(sha256, key, concat(aad, message));253254# Calculates long-term key between users A and B: `get_key(Apriv, Bpub) == get_key(Bpriv, Apub)`255def get_conversation_key(private_key_a, public_key_b):256 shared_x = secp256k1_ecdh(private_key_a, public_key_b)257 return hkdf_extract(IKM=shared_x, salt=utf8_encode('nip44-v2'))258259# Calculates unique per-message key260def get_message_keys(conversation_key, nonce):261 if len(conversation_key) != 32: raise Exception('invalid conversation_key length')262 if len(nonce) != 32: raise Exception('invalid nonce length')263 keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76)264 chacha_key = keys[0:32]265 chacha_nonce = keys[32:44]266 hmac_key = keys[44:76]267 return (chacha_key, chacha_nonce, hmac_key)268269def encrypt(plaintext, conversation_key, nonce):270 (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)271 padded = pad(plaintext)272 ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded)273 mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)274 return base64_encode(concat(write_u8(2), nonce, ciphertext, mac))275276def decrypt(payload, conversation_key):277 (nonce, ciphertext, mac) = decode_payload(payload)278 (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)279 calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)280 if not is_equal_ct(calculated_mac, mac): raise Exception('invalid MAC')281 padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext)282 return unpad(padded_plaintext)283284# Usage:285# conversation_key = get_conversation_key(sender_privkey, recipient_pubkey)286# nonce = secure_random_bytes(32)287# payload = encrypt('hello world', conversation_key, nonce)288# 'hello world' == decrypt(payload, conversation_key)289```290291### Audit292293The v2 of the standard was audited by [Cure53](https://cure53.de) in December 2023.294Check out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf)295and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf).296297### Tests and code298299A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.300301We publish extensive test vectors. Instead of having it in the document directly, a sha256 checksum of vectors is provided:302303 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json304305Example of a test vector from the file:306307```json308{309 "sec1": "0000000000000000000000000000000000000000000000000000000000000001",310 "sec2": "0000000000000000000000000000000000000000000000000000000000000002",311 "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",312 "nonce": "0000000000000000000000000000000000000000000000000000000000000001",313 "plaintext": "a",314 "payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"315}316```317318The file also contains intermediate values. A quick guidance with regards to its usage:319320- `valid.get_conversation_key`: calculate conversation_key from secret key sec1 and public key pub2321- `valid.get_message_keys`: calculate chacha_key, chacha_nonce, hmac_key from conversation_key and nonce322- `valid.calc_padded_len`: take unpadded length (first value), calculate padded length (second value)323- `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.324- `valid.encrypt_decrypt_long_msg`: same as previous step, but instead of a full plaintext and payload, their checksum is provided.325- `invalid.encrypt_msg_lengths`326- `invalid.get_conversation_key`: calculating conversation_key must throw an error327- `invalid.decrypt`: decrypting message content must throw an error328329#### Extended length prefix test vectors330331The following test vectors exercise the boundary between the 2-byte u16 prefix and the 6-byte332extended prefix. Since the payloads are too large to include inline, SHA-256 checksums of the333plaintext and base64-encoded payload are provided (following the `encrypt_decrypt_long_msg` pattern).334335All vectors use the same `conversation_key` and `nonce` as above. Plaintext is the byte `0x61`336(`'a'`) repeated to the specified length.337338```339conversation_key: c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d340nonce: 0000000000000000000000000000000000000000000000000000000000000001341```342343| plaintext_len | prefix | padded_len | plaintext_sha256 | payload_sha256 |344|---|---|---|---|---|345| 65535 | u16 (2 bytes) | 65536 | `6e1bebca6a8229364a162a72ef064826c4cd7457bf54f190ef782bd9deff3e42` | `6d8c2810d1e870fbaa1f0a0937126cca837a15f9260e27060c331d70a3c0bc84` |346| 65536 | extended (6 bytes) | 65536 | `bf718b6f653bebc184e1479f1935b8da974d701b893afcf49e701f3e2f9f9c5a` | `b7b4edb36ba92e267d322d56d9aebc22e7fa96ff52e3c12adc07f07a43cbc616` |347| 65537 | extended (6 bytes) | 81920 | `008ffc88d3c96a9f307524eb361e47c5222a887fc45fa0c1fb8d429c5c23b430` | `eeb7c7c5373894ea2c1547cfd3ccb15d5a0b2d619da852e5c79df792dcc9e435` |348349Note that 65535 and 65536 both have a `padded_len` of 65536, but the total padded-with-prefix350sizes differ: 65538 (2 + 65536) vs 65542 (6 + 65536). The jump to 65537 triggers the next351padding bucket at 81920.