{"id":13832709,"url":"https://github.com/paulmillr/nip44","last_synced_at":"2025-04-15T21:17:27.365Z","repository":{"id":198840970,"uuid":"700760645","full_name":"paulmillr/nip44","owner":"paulmillr","description":"NIP44 encrypted messages for nostr. Spec and implementations","archived":false,"fork":false,"pushed_at":"2024-12-04T19:17:19.000Z","size":1377,"stargazers_count":31,"open_issues_count":1,"forks_count":13,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-15T21:17:18.962Z","etag":null,"topics":["encrypted","encryption","fsharp","go","kotlin","nip44","nostr","payload","rust","secure","typescript"],"latest_commit_sha":null,"homepage":"https://nostr.com","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/paulmillr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":"audit-2023.12.pdf","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-05T08:39:54.000Z","updated_at":"2025-03-01T00:34:25.000Z","dependencies_parsed_at":null,"dependency_job_id":"c382426f-8370-492b-ba11-9f1c59fc65d3","html_url":"https://github.com/paulmillr/nip44","commit_stats":null,"previous_names":["paulmillr/nip44-implementations"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulmillr%2Fnip44","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulmillr%2Fnip44/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulmillr%2Fnip44/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paulmillr%2Fnip44/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/paulmillr","download_url":"https://codeload.github.com/paulmillr/nip44/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249153953,"owners_count":21221330,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["encrypted","encryption","fsharp","go","kotlin","nip44","nostr","payload","rust","secure","typescript"],"created_at":"2024-08-04T11:00:28.468Z","updated_at":"2025-04-15T21:17:27.357Z","avatar_url":"https://github.com/paulmillr.png","language":"C","funding_links":[],"categories":["Libraries"],"sub_categories":["Client reviews and/or comparisons"],"readme":"# NIP-44\n\nNIP44 encrypted messages for [nostr](https://nostr.com).\nSpec copied from [github.com/nostr-protocol/nips](https://github.com/nostr-protocol/nips/blob/master/44.md).\nAudited on [2023.12](./audit-2023.12.pdf) by Cure53, Dr M. Heiderich, Dr. Mazaheri, Dr. Bleichenbacher.\n\nNot all implementations have been audited. Only the TS/JS, Go, \u0026 Rust repos were included in the 2023.12 audit. Be sure to check the commits in scope of the audit \u0026 use at your own risk.\n\n| Language        | License       | Copied from                                |\n|-----------------|---------------|--------------------------------------------|\n| TypeScript / JS | Public domain | https://github.com/nostr-protocol/nips     |\n| C               | LGPL 2.1+     | https://github.com/vnuge/noscrypt          |\n| Dart            | MIT           | https://github.com/chebizarro/dart-nip44   |\n| F#              | GPL 2         | https://github.com/lontivero/Nostra        |\n| Go              | MIT           | https://github.com/ekzyis/nip44            |\n| Haskell         | GPL 3         | https://github.com/futrnostr/futr          |\n| Kotlin          | MIT           | https://github.com/vitorpamplona/amethyst  |\n| Rust            | MIT           | https://github.com/mikedilger/nip44        |\n| Swift           | MIT           | https://github.com/nostr-sdk/nostr-sdk-ios |\n\n---\nNIP-44\n=====\n\nEncrypted Payloads (Versioned)\n------------------------------\n\n`optional`\n\nThe NIP introduces a new data format for keypair-based encryption. This NIP is versioned\nto allow multiple algorithm choices to exist simultaneously. This format may be used for\nmany things, but MUST be used in the context of a signed event as described in NIP 01.\n\n*Note*: this format DOES NOT define any `kind`s related to a new direct messaging standard,\nonly the encryption required to define one. It SHOULD NOT be used as a drop-in replacement\nfor NIP 04 payloads.\n\n## Versions\n\nCurrently defined encryption algorithms:\n\n- `0x00` - Reserved\n- `0x01` - Deprecated and undefined\n- `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64\n\n## Limitations\n\nEvery nostr user has their own public key, which solves key distribution problems present\nin other solutions. However, nostr's relay-based architecture makes it difficult to implement\nmore robust private messaging protocols with things like metadata hiding, forward secrecy,\nand post compromise secrecy.\n\nThe goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed\nevent. When applying this NIP to any use case, it's important to keep in mind your users' threat\nmodel and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE\nmessaging software and limit use of nostr to exchanging contacts.\n\nOn its own, messages sent using this scheme have a number of important shortcomings:\n\n- No deniability: it is possible to prove an event was signed by a particular key\n- No forward secrecy: when a key is compromised, it is possible to decrypt all previous conversations\n- No post-compromise security: when a key is compromised, it is possible to decrypt all future conversations\n- No post-quantum security: a powerful quantum computer would be able to decrypt the messages\n- IP address leak: user IP may be seen by relays and all intermediaries between user and relay\n- Date leak: `created_at` is public, since it is a part of NIP 01 event\n- Limited message size leak: padding only partially obscures true message length\n- No attachments: they are not supported\n\nLack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking\nrelays to delete stored messages after a certain duration has elapsed.\n\n## Version 2\n\nNIP-44 version 2 has the following design characteristics:\n\n- Payloads are authenticated using a MAC before signing rather than afterwards because events are assumed\n  to be signed as specified in NIP-01. The outer signature serves to authenticate the full payload, and MUST\n  be validated before decrypting.\n- ChaCha is used instead of AES because it's faster and has\n  [better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/).\n- ChaCha is used instead of XChaCha because XChaCha has not been standardized. Also, xChaCha's improved collision\n  resistance of nonces isn't necessary since every message has a new (key, nonce) pair.\n- HMAC-SHA256 is used instead of Poly1305 because polynomial MACs are much easier to forge.\n- SHA256 is used instead of SHA3 or BLAKE because it is already used in nostr. Also BLAKE's speed advantage\n  is smaller in non-parallel environments.\n- A custom padding scheme is used instead of padmé because it provides better leakage reduction for small messages.\n- Base64 encoding is used instead of another compression algorithm because it is widely available, and is already used in nostr.\n\n### Encryption\n\n1. Calculate a conversation key\n   - Execute ECDH (scalar multiplication) of public key B by private key A\n     Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point\n   - Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')`\n   - HKDF output will be a `conversation_key` between two users.\n   - It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)`\n2. Generate a random 32-byte nonce\n   - Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator)\n   - Don't generate a nonce from message content\n   - Don't re-use the same nonce between messages: doing so would make them decryptable,\n     but won't leak the long-term key\n3. Calculate message keys\n   - The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes long\n   - Use HKDF-expand, with sha256, `PRK=conversation_key`, `info=nonce` and `L=76`\n   - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76)\n4. Add padding\n   - Content must be encoded from UTF-8 into byte array\n   - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes\n   - Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]`\n   - Padding algorithm is related to powers-of-two, with min padded msg size of 32\n   - Plaintext length is encoded in big-endian as first 2 bytes of the padded blob\n5. Encrypt padded content\n   - Use ChaCha20, with key and nonce from step 3\n6. Calculate MAC (message authentication code)\n   - AAD (additional authenticated data) is used - instead of calculating MAC on ciphertext,\n     it's calculated over a concatenation of `nonce` and `ciphertext`\n   - Validate that AAD (nonce) is 32 bytes\n7. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)`\n\nEncrypted payloads MUST be included in an event's payload, hashed, and signed as defined in NIP 01, using schnorr\nsignature scheme over secp256k1.\n\n### Decryption\n\nBefore decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be\na valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact\nvalidation rules, refer to BIP-340.\n\n1. Check if first payload's character is `#`\n   - `#` is an optional future-proof flag that means non-base64 encoding is used\n   - The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`,\n     implementations MUST indicate that the encryption version is not yet supported\n2. Decode base64\n   - Base64 is decoded into `version, nonce, ciphertext, mac`\n   - If the version is unknown, implementations must indicate that the encryption version is not supported\n   - Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars\n   - Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes\n3. Calculate conversation key\n   - See step 1 of [encryption](#Encryption)\n4. Calculate message keys\n   - See step 3 of [encryption](#Encryption)\n5. Calculate MAC (message authentication code) with AAD and compare\n   - Stop and throw an error if MAC doesn't match the decoded one from step 2\n   - Use constant-time comparison algorithm\n6. Decrypt ciphertext\n   - Use ChaCha20 with key and nonce from step 3\n7. Remove padding\n   - Read the first two BE bytes of plaintext that correspond to plaintext length\n   - Verify that the length of sliced plaintext matches the value of the two BE bytes\n   - Verify that calculated padding from step 3 of the [encryption](#Encryption) process matches the actual padding\n\n### Details\n\n- Cryptographic methods\n  - `secure_random_bytes(length)` fetches randomness from CSPRNG.\n  - `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869)\n    with SHA256 hash function comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)`.\n  - `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439) with\n    starting counter set to 0.\n  - `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104).\n  - `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a ⋅ B`), defined in\n    [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki).\n    The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method\n    `bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid,\n    on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]`.\n    NIP44 doesn't do hashing of the output: keep this in mind, because some libraries hash it using sha256.\n    As an example, in libsecp256k1, unhashed version is available in `secp256k1_ec_pubkey_tweak_mul`\n- Operators\n  - `x[i:j]`, where `x` is a byte array and `i, j \u003c= 0` returns a `(j - i)`-byte array with a copy of the\n    `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`.\n- Constants `c`:\n  - `min_plaintext_size` is 1. 1b msg is padded to 32b.\n  - `max_plaintext_size` is 65535 (64kb - 1). It is padded to 65536.\n- Functions\n  - `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding)\n  - `concat` refers to byte array concatenation\n  - `is_equal_ct(a, b)` is constant-time equality check of 2 byte arrays\n  - `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back\n  - `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array\n  - `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array\n  - `zeros(length)` creates byte array of length `length \u003e= 0`, filled with zeros\n  - `floor(number)` and `log2(number)` are well-known mathematical methods\n\n### Implementation pseudocode\n\nThe following is a collection of python-like pseudocode functions which implement the above primitives,\nintended to guide implementers. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.\n\n```py\n# Calculates length of the padded byte array.\ndef calc_padded_len(unpadded_len):\n  next_power = 1 \u003c\u003c (floor(log2(unpadded_len - 1))) + 1\n  if next_power \u003c= 256:\n    chunk = 32\n  else:\n    chunk = next_power / 8\n  if unpadded_len \u003c= 32:\n    return 32\n  else:\n    return chunk * (floor((len - 1) / chunk) + 1)\n\n# Converts unpadded plaintext to padded bytearray\ndef pad(plaintext):\n  unpadded = utf8_encode(plaintext)\n  unpadded_len = len(plaintext)\n  if (unpadded_len \u003c c.min_plaintext_size or\n      unpadded_len \u003e c.max_plaintext_size): raise Exception('invalid plaintext length')\n  prefix = write_u16_be(unpadded_len)\n  suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len)\n  return concat(prefix, unpadded, suffix)\n\n# Converts padded bytearray to unpadded plaintext\ndef unpad(padded):\n  unpadded_len = read_uint16_be(padded[0:2])\n  unpadded = padded[2:2+unpadded_len]\n  if (unpadded_len == 0 or\n      len(unpadded) != unpadded_len or\n      len(padded) != 2 + calc_padded_len(unpadded_len)): raise Exception('invalid padding')\n  return utf8_decode(unpadded)\n\n# metadata: always 65b (version: 1b, nonce: 32b, max: 32b)\n# plaintext: 1b to 0xffff\n# padded plaintext: 32b to 0xffff\n# ciphertext: 32b+2 to 0xffff+2\n# raw payload: 99 (65+32+2) to 65603 (65+0xffff+2)\n# compressed payload (base64): 132b to 87472b\ndef decode_payload(payload):\n  plen = len(payload)\n  if plen == 0 or payload[0] == '#': raise Exception('unknown version')\n  if plen \u003c 132 or plen \u003e 87472: raise Exception('invalid payload size')\n  data = base64_decode(payload)\n  dlen = len(d)\n  if dlen \u003c 99 or dlen \u003e 65603: raise Exception('invalid data size');\n  vers = data[0]\n  if vers != 2: raise Exception('unknown version ' + vers)\n  nonce = data[1:33]\n  ciphertext = data[33:dlen - 32]\n  mac = data[dlen - 32:dlen]\n  return (nonce, ciphertext, mac)\n\ndef hmac_aad(key, message, aad):\n  if len(aad) != 32: raise Exception('AAD associated data must be 32 bytes');\n  return hmac(sha256, key, concat(aad, message));\n\n# Calculates long-term key between users A and B: `get_key(Apriv, Bpub) == get_key(Bpriv, Apub)`\ndef get_conversation_key(private_key_a, public_key_b):\n  shared_x = secp256k1_ecdh(private_key_a, public_key_b)\n  return hkdf_extract(IKM=shared_x, salt=utf8_encode('nip44-v2'))\n\n# Calculates unique per-message key\ndef get_message_keys(conversation_key, nonce):\n  if len(conversation_key) != 32: raise Exception('invalid conversation_key length')\n  if len(nonce) != 32: raise Exception('invalid nonce length')\n  keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76)\n  chacha_key = keys[0:32]\n  chacha_nonce = keys[32:44]\n  hmac_key = keys[44:76]\n  return (chacha_key, chacha_nonce, hmac_key)\n\ndef encrypt(plaintext, conversation_key, nonce):\n  (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)\n  padded = pad(plaintext)\n  ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded)\n  mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)\n  return base64_encode(concat(write_u8(2), nonce, ciphertext, mac))\n\ndef decrypt(payload, conversation_key):\n  (nonce, ciphertext, mac) = decode_payload(payload)\n  (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)\n  calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)\n  if not is_equal_ct(calculated_mac, mac): raise Exception('invalid MAC')\n  padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext)\n  return unpad(padded_plaintext)\n\n# Usage:\n#   conversation_key = get_conversation_key(sender_privkey, recipient_pubkey)\n#   nonce = secure_random_bytes(32)\n#   payload = encrypt('hello world', conversation_key, nonce)\n#   'hello world' == decrypt(payload, conversation_key)\n```\n\n### Audit\n\nThe v2 of the standard was audited by [Cure53](https://cure53.de) in December 2023.\nCheck out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf)\nand [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf).\n\n### Tests and code\n\nA collection of implementations in different languages is available at https://github.com/paulmillr/nip44.\n\nWe publish extensive test vectors. Instead of having it in the document directly, a sha256 checksum of vectors is provided:\n\n    269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040  nip44.vectors.json\n\nExample of a test vector from the file:\n\n```json\n{\n  \"sec1\": \"0000000000000000000000000000000000000000000000000000000000000001\",\n  \"sec2\": \"0000000000000000000000000000000000000000000000000000000000000002\",\n  \"conversation_key\": \"c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d\",\n  \"nonce\": \"0000000000000000000000000000000000000000000000000000000000000001\",\n  \"plaintext\": \"a\",\n  \"payload\": \"AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb\"\n}\n```\n\nThe file also contains intermediate values. A quick guidance with regards to its usage:\n\n- `valid.get_conversation_key`: calculate conversation_key from secret key sec1 and public key pub2\n- `valid.get_message_keys`: calculate chacha_key, chacha_nonce, hmac_key from conversation_key and nonce\n- `valid.calc_padded_len`: take unpadded length (first value), calculate padded length (second value)\n- `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.\n- `valid.encrypt_decrypt_long_msg`: same as previous step, but instead of a full plaintext and payload, their checksum is provided.\n- `invalid.encrypt_msg_lengths`\n- `invalid.get_conversation_key`: calculating conversation_key must throw an error\n- `invalid.decrypt`: decrypting message content must throw an error\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaulmillr%2Fnip44","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaulmillr%2Fnip44","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaulmillr%2Fnip44/lists"}