From 7f98a7b8849c16f9d324e0ea15db4efda0ee84fb Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Fri, 11 Aug 2023 08:34:18 -0700 Subject: [PATCH 1/3] Introduce NIP-44 encryption standard --- 04.md | 4 +- 07.md | 6 +- 44.md | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 46.md | 10 ++- README.md | 3 +- 5 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 44.md diff --git a/04.md b/04.md index 6e45b74b5e..2490c18338 100644 --- a/04.md +++ b/04.md @@ -1,10 +1,12 @@ +> __Warning__ `unrecommended`: deprecated in favor of [NIP-44](44.md) + NIP-04 ====== Encrypted Direct Message ------------------------ -`final` `optional` `author:arcbtc` +`final` `unrecommended` `author:arcbtc` A special event with kind `4`, meaning "encrypted direct message". It is supposed to have the following attributes: diff --git a/07.md b/07.md index ee4e3722cc..55f78c5750 100644 --- a/07.md +++ b/07.md @@ -18,8 +18,10 @@ async window.nostr.signEvent(event: Event): Event // takes an event object, adds Aside from these two basic above, the following functions can also be implemented optionally: ``` async window.nostr.getRelays(): { [url: string]: {read: boolean, write: boolean} } // returns a basic map of relay urls to relay policies -async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 -async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 +async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated) +async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated) +async window.nostr.nip44.encrypt(pubkey, plaintext, version): string // returns encrypted payload as specified in nip-44 +async window.nostr.nip44.decrypt(pubkey, payload): string // takes encrypted payload as specified in nip-44 ``` ### Implementation diff --git a/44.md b/44.md new file mode 100644 index 0000000000..651960528c --- /dev/null +++ b/44.md @@ -0,0 +1,255 @@ +NIP-44 +====== + +Encrypted Payloads (Versioned) +------------------------------ + +`optional` `author:paulmillr` `author:staab` + +The NIP introduces a new data format for keypair-based encryption. This NIP is versioned to allow multiple algorithm choices to exist simultaneously. + +The encrypted payload is encoded as base64 using the character set defined in RFC4648. The first byte of the payload represents the version. The rest depends on the version. + +Currently defined encryption algorithms: + +- `0x00` - Reserved +- `0x01` - XChaCha20 with same key `sha256(parity + ecdh)` per conversation + +# Version 1 + +Encoding: + +- `1` - 1 byte, version +- `nonce` - 24 bytes fetched from a cryptographically secure random source +- `ciphertext` - variable number of bytes until the end + +`ciphertext` is created using `xchacha20(key, nonce, plaintext)`. `key` is any private key, either randomly generated or derived from a sender's private key and a recipient's public key. + +Example: + +- Alice's private key: `5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a` +- Bob's private key: `4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d` + +Encrypting the message `hello` from Alice to Bob results in the following base-64 encoded payload: + +``` +Abd8jOLZT0OAEE6kZ5hI1qd1ZRrVR1W46vRyZCL5 +``` + +# Other Notes + +By default in the [libsecp256k1](https://github.com/bitcoin-core/secp256k1) ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). We are using this exact implementation. In NIP-04, unhashed shared point was used. This encryption scheme replaces the one described in NIP-04, which is not secure. It used bad cryptographic building blocks and must not be used. + +XChaCha20, being a stream cipher, does not provide integrity guarantees. This encryption scheme should only be used in tandem with a signature or HMAC to verify integrity. Public and private keys should also be checked before validity when deriving a shared key. + +# Code Samples + +## Javascript + +```javascript +import {base64} from '@scure/base' +import {randomBytes} from '@noble/hashes/utils' +import {secp256k1} from '@noble/curves/secp256k1' +import {sha256} from '@noble/hashes/sha256' +import {xchacha20} from '@noble/ciphers/chacha' + +const utf8Decoder = new TextDecoder() + +const utf8Encoder = new TextEncoder() + +export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array => + sha256(secp256k1.getSharedSecret(privkey, '02' + pubkey).subarray(1, 33)) + +export function encrypt(key: Uint8Array, text: string, v = 1) { + if (v !== 1) { + throw new Error('NIP44: unknown encryption version') + } + + const nonce = randomBytes(24) + const plaintext = utf8Encoder.encode(text) + const ciphertext = xchacha20(key, nonce, plaintext) + + const payload = new Uint8Array(25 + ciphertext.length) + payload.set([v], 0) + payload.set(nonce, 1) + payload.set(ciphertext, 25) + + return base64.encode(payload) +} + +export function decrypt(key: string, payload: string) { + let data + try { + data = base64.decode(payload) + } catch (e) { + throw new Error(`NIP44: failed to base64 decode payload: ${e}`) + } + + if (data[0] !== 1) { + throw new Error(`NIP44: unknown encryption version: ${data[0]}`) + } + + const nonce = data.slice(1, 25) + const ciphertext = data.slice(25) + const plaintext = xchacha20(key, nonce, ciphertext) + + return utf8Decoder.decode(plaintext) +} +``` + +## Kotlin + +```kotlin +// implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1' +// implementation "com.goterl:lazysodium-android:5.1.0@aar" +// implementation "net.java.dev.jna:jna:5.12.1@aar" + +fun getSharedSecretNIP44(privKey: ByteArray, pubKey: ByteArray): ByteArray = + MessageDigest.getInstance("SHA-256").digest( + Secp256k1.get().pubKeyTweakMul( + Hex.decode("02") + pubKey, + privKey + ).copyOfRange(1, 33) + ) + +fun encryptNIP44(msg: String, privKey: ByteArray, pubKey: ByteArray): String { + val nonce = ByteArray(24).apply { + SecureRandom.getInstanceStrong().nextBytes(this) + } + + val cipher = streamXChaCha20Xor( + message = msg.toByteArray(), + nonce = nonce, + key = getSharedSecretNIP44(privKey, pubKey) + ) + + return Base64.getEncoder().encodeToString( + byteArrayOf(Nip44Version.XChaCha20.code) + nonce + cipher + ) +} + +fun decryptNIP44(base64: String, privKey: ByteArray, pubKey: ByteArray): String? { + val byteArray = Base64.getDecoder().decode(base64) + + require(byteArray[0].toInt() == Nip44Version.XChaCha20.code) { "NIP44: unknown encryption version" } + + return streamXChaCha20Xor( + message = byteArray.copyOfRange(25, byteArray.size), + nonce = byteArray.copyOfRange(1, 25), + key = getSharedSecretNIP44(privKey, pubKey) + )?.decodeToString() +} + +// This method is not exposed in AndroidSodium yet, but it will be in the next version. +fun streamXChaCha20Xor(message: ByteArray, nonce: ByteArray, key: ByteArray): ByteArray? { + return with (SodiumAndroid()) { + val resultCipher = ByteArray(message.size) + + val isSuccessful = crypto_stream_chacha20_xor_ic( + resultCipher, + message, + message.size.toLong(), + nonce.drop(16).toByteArray(), // chacha nonce is just the last 8 bytes. + 0, + ByteArray(32).apply { + crypto_core_hchacha20(this, nonce, key, null) + } + ) == 0 + + if (isSuccessful) resultCipher else null + } +} + +enum class Nip44Version(val code: Int) { + Reserved(0), + XChaCha20(1) +} +``` + +# Test Vectors + +``` +{ + "valid_sec": [ + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "sec2": "0000000000000000000000000000000000000000000000000000000000000002", + "shared": "0135da2f8acf7b9e3090939432e47684eb888ea38c2173054d4eedffdf152ca5", + "nonce": "121f9d60726777642fd82286791ab4d7461c9502ebcbb6e6", + "plaintext": "a", + "ciphertext": "ARIfnWByZ3dkL9gihnkatNdGHJUC68u25qM=", + "note": "sk1 = 1, sk2 = random, 0x02" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "sec2": "0000000000000000000000000000000000000000000000000000000000000001", + "shared": "0135da2f8acf7b9e3090939432e47684eb888ea38c2173054d4eedffdf152ca5", + "plaintext": "a", + "ciphertext": "AeCt7jJ8L+WBOTiCSfeXEGXB/C/wgsrSRek=", + "nonce": "e0adee327c2fe58139388249f7971065c1fc2ff082cad245", + "note": "sk1 = 1, sk2 = random, 0x02" + } + ], + "valid_pub": [ + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "0000000000000000000000000000000000000000000000000000000000000002", + "shared": "a6d6a2f7011cdd1aeef325948f48c6efa40f0ec723ae7f5ac7e3889c43481500", + "nonce": "f481750e13dfa90b722b7cce0db39d80b0db2e895cc3001a", + "plaintext": "a", + "ciphertext": "AfSBdQ4T36kLcit8zg2znYCw2y6JXMMAGjM=", + "note": "sec1 = n-2, pub2: random, 0x02" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb", + "shared": "4908464f77dd74e11a9b4e4a3bc2467445bd794e8abcbfafb65a6874f9e25a8f", + "nonce": "45c484ba2c0397853183adba6922156e09a2ad4e3e6914f2", + "plaintext": "A Peer-to-Peer Electronic Cash System", + "ciphertext": "AUXEhLosA5eFMYOtumkiFW4Joq1OPmkU8k/25+3+VDFvOU39qkUDl1aiy8Q+0ozTwbhD57VJoIYayYS++hE=", + "note": "sec1 = 2, pub2: " + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "shared": "132f39a98c31baaddba6525f5d43f2954472097fa15265f45130bfdb70e51def", + "nonce": "d60de08405cf9bde508147e82224ac6af409c12b9e5492e1", + "plaintext": "A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution. Digital signatures provide part of the solution, but the main benefits are lost if a trusted third party is still required to prevent double-spending.", + "ciphertext": "AdYN4IQFz5veUIFH6CIkrGr0CcErnlSS4VdvoQaP2DCB1dIFL72HSriG1aFABcTlu86hrsG0MdOO9rPdVXc3jptMMzqvIN6tJlHPC8GdwFD5Y8BT76xIIOTJR2W0IdrM7++WC/9harEJAdeWHDAC9zNJX81CpCz4fnV1FZ8GxGLC0nUF7NLeUiNYu5WFXQuO9uWMK0pC7tk3XVogk90X6rwq0MQG9ihT7e1elatDy2YGat+VgQlDrz8ZLRw/lvU+QqeXMQgjqn42sMTrimG6NdKfHJSVWkT6SKZYVsuTyU1Iu5Nk0twEV8d11/MPfsMx4i36arzTC9qxE6jftpOoG8f/jwPTSCEpHdZzrb/CHJcpc+zyOW9BZE2ZOmSxYHAE0ustC9zRNbMT3m6LqxIoHq8j+8Ysu+Cwqr4nUNLYq/Q31UMdDg1oamYS17mWIAS7uf2yF5uT5IlG", + "note": "sec1 == pub2" + } + ], + "invalid": [ + { + "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "plaintext": "a", + "note": "sec1 higher than curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000000", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "plaintext": "a", + "note": "sec1 is 0" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "plaintext": "a", + "note": "pub2 is invalid, no sqrt, all-ff" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "plaintext": "a", + "note": "sec1 == curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "plaintext": "a", + "note": "pub2 is invalid, no sqrt" + } + ] +} +``` diff --git a/46.md b/46.md index 90fa1a063c..7f4eb0f8f3 100644 --- a/46.md +++ b/46.md @@ -82,12 +82,18 @@ These are mandatory methods the remote signer app MUST implement: - **get_relays** - params [] - result `{ [url: string]: {read: boolean, write: boolean} }` -- **nip04_encrypt** +- **nip04_encrypt** (deprecated) - params [`pubkey`, `plaintext`] - result `nip4 ciphertext` -- **nip04_decrypt** +- **nip04_decrypt** (deprecated) - params [`pubkey`, `nip4 ciphertext`] - result [`plaintext`] +- **nip44_encrypt** + - params [`pubkey`, `plaintext`, `version`] + - result `nip44 encrypted payload` +- **nip44_decrypt** + - params [`pubkey`, `nip44 encrypted payload`] + - result [`plaintext`] NOTICE: `pubkey` and `signature` are hex-encoded strings. diff --git a/README.md b/README.md index f8b77dfbcc..f54a1585e3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos - [NIP-01: Basic protocol flow description](01.md) - [NIP-02: Contact List and Petnames](02.md) - [NIP-03: OpenTimestamps Attestations for Events](03.md) -- [NIP-04: Encrypted Direct Message](04.md) +- [NIP-04: Encrypted Direct Message](04.md) --- **unrecommended**: deprecated in favor of [NIP-44](44.md) - [NIP-05: Mapping Nostr keys to DNS-based internet identifiers](05.md) - [NIP-06: Basic key derivation from mnemonic seed phrase](06.md) - [NIP-07: `window.nostr` capability for web browsers](07.md) @@ -49,6 +49,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos - [NIP-39: External Identities in Profiles](39.md) - [NIP-40: Expiration Timestamp](40.md) - [NIP-42: Authentication of clients to relays](42.md) +- [NIP-44: Versioned Encryption](44.md) - [NIP-45: Counting results](45.md) - [NIP-46: Nostr Connect](46.md) - [NIP-47: Wallet Connect](47.md) From 1728d96bb7794c8866a59edb9ce82315548aad36 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Sat, 7 Oct 2023 07:43:50 +0000 Subject: [PATCH 2/3] Finalize NIP-44 --- 07.md | 4 +- 44.md | 605 +++++++++++++++++++++++++++++++++++++--------------------- 46.md | 5 +- 3 files changed, 393 insertions(+), 221 deletions(-) diff --git a/07.md b/07.md index 55f78c5750..b76332334e 100644 --- a/07.md +++ b/07.md @@ -20,8 +20,8 @@ Aside from these two basic above, the following functions can also be implemente async window.nostr.getRelays(): { [url: string]: {read: boolean, write: boolean} } // returns a basic map of relay urls to relay policies async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated) async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated) -async window.nostr.nip44.encrypt(pubkey, plaintext, version): string // returns encrypted payload as specified in nip-44 -async window.nostr.nip44.decrypt(pubkey, payload): string // takes encrypted payload as specified in nip-44 +async window.nostr.nip44.encrypt(sender_privkey, recipient_pubkey, plaintext, version): string // returns encrypted payload as specified in nip-44 +async window.nostr.nip44.decrypt(recipient_privkey, sender_pubkey, payload): string // takes encrypted payload as specified in nip-44 ``` ### Implementation diff --git a/44.md b/44.md index 651960528c..069b68e845 100644 --- a/44.md +++ b/44.md @@ -8,248 +8,421 @@ Encrypted Payloads (Versioned) The NIP introduces a new data format for keypair-based encryption. This NIP is versioned to allow multiple algorithm choices to exist simultaneously. -The encrypted payload is encoded as base64 using the character set defined in RFC4648. The first byte of the payload represents the version. The rest depends on the version. +nostr is a key directory. Every nostr user has their own public key, +which solves key distribution problem, present in other solutions. +The main goal is to have at least *some* way to send messages between nostr accounts that cannot be read by everyone. -Currently defined encryption algorithms: - -- `0x00` - Reserved -- `0x01` - XChaCha20 with same key `sha256(parity + ecdh)` per conversation +The scheme is not ideal, there are several points to consider: -# Version 1 +- No deniability: it is possible to prove the event was signed by a particular key +- No forward secrecy: when a user key is compromised, it is possible to decrypt all previous conversations +- No post-compromise security: when a user key is compromised, it is possible to decrypt all future conversations +- No post-quantum security: a powerful quantum computer would be able to decrypt the messages +- Timing leak: Message date is public +- Location leak: User IP would be seen by relay and all intermediaries between user and relay +- Message length leak: Padding is obscuring true message length, but not entirely; attachments are not supported -Encoding: +For risky situations, users should chat in specialized E2EE messaging software and limit nostr to exchanging contacts. -- `1` - 1 byte, version -- `nonce` - 24 bytes fetched from a cryptographically secure random source -- `ciphertext` - variable number of bytes until the end +# Versions -`ciphertext` is created using `xchacha20(key, nonce, plaintext)`. `key` is any private key, either randomly generated or derived from a sender's private key and a recipient's public key. +Currently defined encryption algorithms: -Example: +- `0x00` - Reserved +- `0x01` - Deprecated +- `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64 -- Alice's private key: `5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a` -- Bob's private key: `4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d` +## Version 2 -Encrypting the message `hello` from Alice to Bob results in the following base-64 encoded payload: +The following cryptographic primitives are used: +```py +chacha20(key, nonce, data) # ChaCha20 (RFC 8439) +hmac_sha256(key, message) # HMAC (RFC 2104) +hkdf_sha256(key, salt, info, length) # HKDF (RFC 5869) +secp256k1_ecdh(privA, pubB) # ECDH over secp256k1 curve, **32-byte output is not hashed** +base64_encode(string), base64_decode(bytes) # Base64 (RFC 4648, with padding) +secure_random_bytes(length) # Fetches randomness from CSPRNG ``` -Abd8jOLZT0OAEE6kZ5hI1qd1ZRrVR1W46vRyZCL5 -``` - -# Other Notes - -By default in the [libsecp256k1](https://github.com/bitcoin-core/secp256k1) ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). We are using this exact implementation. In NIP-04, unhashed shared point was used. This encryption scheme replaces the one described in NIP-04, which is not secure. It used bad cryptographic building blocks and must not be used. - -XChaCha20, being a stream cipher, does not provide integrity guarantees. This encryption scheme should only be used in tandem with a signature or HMAC to verify integrity. Public and private keys should also be checked before validity when deriving a shared key. - -# Code Samples -## Javascript - -```javascript -import {base64} from '@scure/base' -import {randomBytes} from '@noble/hashes/utils' -import {secp256k1} from '@noble/curves/secp256k1' -import {sha256} from '@noble/hashes/sha256' -import {xchacha20} from '@noble/ciphers/chacha' - -const utf8Decoder = new TextDecoder() - -const utf8Encoder = new TextEncoder() - -export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array => - sha256(secp256k1.getSharedSecret(privkey, '02' + pubkey).subarray(1, 33)) - -export function encrypt(key: Uint8Array, text: string, v = 1) { - if (v !== 1) { - throw new Error('NIP44: unknown encryption version') - } +Helper methods: + +```py +# Calculates static long-term key between users A and B. +# key(Apriv, Bpub) == key(Bpriv, Apub) +# Make sure to validate public keys. +# The method is slow, so, the output could be cached. +# Returns 32 bytes representing unhashed x coordinate. +get_conversation_key(private_key_a, public_key_b): + secp256k1_ecdh(private_key_a, public_key_b).slice(1, 33) + +# Calculates encryption and authentication keys for a message. +get_message_keys(conversation_key, salt): + res = hkdf_sha256(conversation_key, salt: salt, info: 'nip44-v2', length: 76) + { "encryption": res.slice(0, 32), "nonce": res.slice(32, 44), "auth": res.slice(44, 76) } + +# Calculates length of the padded byte array. +calc_padding(unpadded_len): + nextpower = 1 << (floor(log2(unpadded_len - 1)) + 1 + chunk = nextpower <= 256 ? 32 : nextpower / 8 + padded_length = unpadded_len <= 32 ? 32 : chunk * (floor((len - 1) / chunk) + 1) + +# Language and platform-specific methods, we don't define them in the spec +utf8_encode(string), utf8_decode(bytes) # Transforms string to byte array and back +write_u16_be(number) # Encodes number into Big-Endian uint16 byte array +zeros(length) # Creates byte array of length "length", filled with zeros. Can be 0-length +``` - const nonce = randomBytes(24) - const plaintext = utf8Encoder.encode(text) - const ciphertext = xchacha20(key, nonce, plaintext) +#### Encryption + +```py +# input: `sender_privkey, recipient_pubkey, plaintext` OR `conversation_key, plaintext` +salt = secure_random_bytes(32) +conversation_key = get_conversation_key(sender_privkey, recipient_pubkey) +keys = get_message_keys(conversation_key, salt) +unpadded = utf8_encode(plaintext) +unpadded_len = plaintext.length +if (unpadded_len == 0 or unpadded_len > 65536 - 128): throw Error('invalid plaintext length') +padded = write_u16_be(unpadded_len) || unpadded || zeros(calc_padding(unpadded_len) - unpadded_len) +ciphertext = chacha20(key: keys["encryption"], nonce: keys["nonce"], data: padded) +mac = hmac_sha256(key: keys["auth"], message: ciphertext) +payload = base64_encode(2 || salt || ciphertext || mac) +``` - const payload = new Uint8Array(25 + ciphertext.length) - payload.set([v], 0) - payload.set(nonce, 1) - payload.set(ciphertext, 25) +1. Generate random 32-byte `salt` + * Secure random (CSPRNG) must always be used for such generation + * Reusing `salt` between messages would make them decryptable, but would not leak long-term key +2. Calculate message keys + * Generate `conversation_key`: take 32 bytes from unhashed secp256k1 ECDH of private key A and public key B, without parity byte + * Pass `conversation_key` to hkdf-sha256, with salt from step 1 and info: `nip44-v2`. HKDF's outputLength must be 76 bytes. + * The HKDF output is sliced into: `encryption_key` (bytes 0..32), `nonce` (bytes 32..44), `auth` (bytes 44..76) +3. Encode plaintext from utf8 to bytes and pad them + * Validate plaintext length. Minimum is 1 byte, maximum is 65536 - 128 bytes + * Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]` + * Padding algorithm is related to powers-of-two, with min padded msg size of 32 + * Plaintext length is encoded in big-endian as first two bytes of the padded blob + * In some cases, there is no padding: for example, 320-byte msg is padded to 320 bytes + * Padding test vectors are provided below: ensure your padding calculator output matches them +5. Produce ciphertext, by calling ChaCha20 +6. Calculate MAC (message authentication code) over ciphertext +7. Base64-encode the version and concatenated params + +#### Decryption + +Before decryption, it is mandatory to validate event's pubkey and signature: `validate_public_key(event.pubkey) and validate_signature(event)` + +```py +# input: `recipient_privkey, sender_pubkey, payload` OR `conversation_key, payload` +if payload[0] == '#': throw Error('unknown version') +d = base64_decode(payload) +vers = d[0] +if (vers != 2): throw Error('unknown version') + +dlen = d.length +salt, ciphertext, mac = d.slice(1, 33), d.slice(33, dlen - 32), d.slice(dlen - 32, dlen) +conversation_key = get_conversation_key(sender_privkey, recipient_pubkey) +keys = get_message_keys(conversation_key, salt) +calculated_mac = hmac_sha256(key: keys["auth"], message: ciphertext) +if (calculated_mac != mac): throw Error('invalid MAC') + +padded = chacha20(key: keys["encryption"], nonce: keys["nonce"], data: ciphertext) +unpadded_len = read_uint16_be(padded.slice(0, 2)) +unpadded = padded_plaintext.slice(2, 2 + unpadded_len) +if (unpadded_len == 0 or + unpadded.length != unpadded_len or + padded.length != 2 + calc_padding(unpadded_len)): throw Error('invalid padding') +plaintext = utf8_decode(unpadded) +``` - return base64.encode(payload) -} +1. Validate the message's pubkey and signature + * public key must be a valid secp256k1 curve point + * signature must be valid secp256k1 schnorr signature; message serialization is specified in NIP1 +2. Check if first payload's character is `#`. Throw a descriptive error if so + * `#` is an optional flag that means non-base64 encoding is used + * Instead of throwing `base64 is invalid`, an app must say the encryption version is not yet supported +3. Decode base64 + * Base64 is decoded into `version, salt, ciphertext, mac` + * If the version is unknown, the app, an app must say the encryption version is not yet supported +4. Calculate message keys + * Reuse algorithm from encryption's step 2 +5. Calculate and compare MAC using `auth_key` from step 4, reusing the algorithm from encryption's step 5 + * Stop and throw an error if MAC doesn't match the decoded one from step 2 +6. Decrypt ciphertext into plaintext, using ChaCha20 +7. Unpad plaintext + * Read the first two BE bytes of plaintext that correspond to plaintext length + * Verify that the length of sliced plaintext matches the value of the two BE bytes + * Verify that calculated padding from encryption's step 3 matches the actual padding -export function decrypt(key: string, payload: string) { - let data - try { - data = base64.decode(payload) - } catch (e) { - throw new Error(`NIP44: failed to base64 decode payload: ${e}`) - } +Example: - if (data[0] !== 1) { - throw new Error(`NIP44: unknown encryption version: ${data[0]}`) - } +- Alice's private key: `5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a` +- Bob's private key: `4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d` +- Message salt: `b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b` - const nonce = data.slice(1, 25) - const ciphertext = data.slice(25) - const plaintext = xchacha20(key, nonce, ciphertext) +Encrypting the message `hello` from Alice to Bob results in the following base-64 encoded payload: - return utf8Decoder.decode(plaintext) -} +``` +ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7ysu7m8bzLLv3LyxbtMit2SsnmvFjnrJN9Qqoenb/M2mwWjcfA92Xeb92ZrTKcaQOi6jdXajWgRcRxO/TWJo93il3 ``` -## Kotlin - -```kotlin -// implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1' -// implementation "com.goterl:lazysodium-android:5.1.0@aar" -// implementation "net.java.dev.jna:jna:5.12.1@aar" - -fun getSharedSecretNIP44(privKey: ByteArray, pubKey: ByteArray): ByteArray = - MessageDigest.getInstance("SHA-256").digest( - Secp256k1.get().pubKeyTweakMul( - Hex.decode("02") + pubKey, - privKey - ).copyOfRange(1, 33) - ) - -fun encryptNIP44(msg: String, privKey: ByteArray, pubKey: ByteArray): String { - val nonce = ByteArray(24).apply { - SecureRandom.getInstanceStrong().nextBytes(this) - } - - val cipher = streamXChaCha20Xor( - message = msg.toByteArray(), - nonce = nonce, - key = getSharedSecretNIP44(privKey, pubKey) - ) +# Implementations - return Base64.getEncoder().encodeToString( - byteArrayOf(Nip44Version.XChaCha20.code) + nonce + cipher - ) -} +A collection of implementations in different languages is available [on GitHub](https://github.com/paulmillr/nip44-implementations). -fun decryptNIP44(base64: String, privKey: ByteArray, pubKey: ByteArray): String? { - val byteArray = Base64.getDecoder().decode(base64) +# Test Vectors - require(byteArray[0].toInt() == Nip44Version.XChaCha20.code) { "NIP44: unknown encryption version" } +Steps that must be tested: - return streamXChaCha20Xor( - message = byteArray.copyOfRange(25, byteArray.size), - nonce = byteArray.copyOfRange(1, 25), - key = getSharedSecretNIP44(privKey, pubKey) - )?.decodeToString() -} +- `valid_sec` - encrypt (calculate and compare shared key, calculate and compare ciphertext), decrypt (compare plaintext) +- `valid_pub` - encrypt (calculate and compare shared key, calculate and compare ciphertext), decrypt (compare plaintext) +- `invalid` - decrypt must throw an error +- `invalid_conversation_key` - encrypt or get_conversation_key must throw an error +- `padding` - tests for calc_padding utility method -// This method is not exposed in AndroidSodium yet, but it will be in the next version. -fun streamXChaCha20Xor(message: ByteArray, nonce: ByteArray, key: ByteArray): ByteArray? { - return with (SodiumAndroid()) { - val resultCipher = ByteArray(message.size) - - val isSuccessful = crypto_stream_chacha20_xor_ic( - resultCipher, - message, - message.size.toLong(), - nonce.drop(16).toByteArray(), // chacha nonce is just the last 8 bytes. - 0, - ByteArray(32).apply { - crypto_core_hchacha20(this, nonce, key, null) +```json +{ + "v2": { + "valid_sec": [ + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "sec2": "0000000000000000000000000000000000000000000000000000000000000002", + "shared": "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "salt": "0000000000000000000000000000000000000000000000000000000000000001", + "plaintext": "a", + "ciphertext": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYNpT9ESckRbRUY7bUF5P+1rObpA4BNoksAUQ8myMDd9/37W/J2YHvBpRjvy9uC0+ovbpLc0WLaMFieqAMdIYqR14", + "note": "sk1 = 1, sk2 = random, 0x02" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "sec2": "0000000000000000000000000000000000000000000000000000000000000001", + "shared": "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "salt": "f00000000000000000000000000000f00000000000000000000000000000000f", + "plaintext": "🍕🫃", + "ciphertext": "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPKY68BwdF7PIT205jBoaZHSs7OMpKsULW5F5ClOJWiy6XjZy7s2v85KugYmbBKgEC2LytbXbxkr7Jpgfk529K3/pP", + "note": "sk1 = 1, sk2 = random, 0x02" + }, + { + "sec1": "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a", + "sec2": "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d", + "shared": "94da47d851b9c1ed33b3b72f35434f56aa608d60e573e9c295f568011f4f50a4", + "salt": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b", + "plaintext": "表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀", + "ciphertext": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7yuU7XwJ8wCYUrq4aXX86HLnkMx7fPFvNeMk0uek9ma01magfEBIf+vJvZdWKiv48eUu9Cv31plAJsH6kSIsGc5TVYBYipkrQUNRxxJA15QT+uCURF96v3XuSS0k2Pf108AI=", + "note": "unicode-heavy string" + }, + { + "sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c", + "sec2": "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba", + "shared": "ab99c122d4586cdd5c813058aa543d0e7233545dbf6874fc34a3d8d9a18fbbc3", + "salt": "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf", + "plaintext": "ability🤝的 ȺȾ", + "ciphertext": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvPSc+7YCIFTmGk5OLuh1nhl6TvID7sGKLFUCWRW1eRfV/0a7sT46N3nTQzD7IE67zLWrYqGnE+0DDNz6sJ4hAaFrT" + }, + { + "sec1": "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c", + "sec2": "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae", + "shared": "a449f2a85c6d3db0f44c64554a05d11a3c0988d645e4b4b2592072f63662f422", + "salt": "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1", + "plaintext": "pepper👀їжак", + "ciphertext": "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGx1TkkMfiHJxEeSdQ/4Rlaghn0okDCNYLihBsHrDzBsNRC27APmH9mmZcpcg66Mb0exH9V5/lLBWdQW+fcY9GpvXv0" + }, + { + "sec1": "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f", + "sec2": "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3", + "shared": "decde9938ffcb14fa7ff300105eb1bf239469af9baf376e69755b9070ae48c47", + "salt": "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407", + "plaintext": "( ͡° ͜ʖ ͡°)", + "ciphertext": "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHiR8Hljs6Nl/XsNDAmCz6U1Z3NUGhbCtczc3wXXxDzFkjjMimxsf/74OEzu7LphUadM9iSWvVKPrNXY7lTD0B2muz" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", + "salt": "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68", + "plaintext": "مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،", + "ciphertext": "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItohfde4vHVRHUupr+Glh9JW4f9EY+w795hvRZbixs0EQgDZ7zwLlymVQI3NNvMqvemQzHUA1I5+9gSu8XSMwX9gDCUAjUJtntCkRt9+tjdy2Wa2ZrDYqCvgirvzbJTIC69Ve3YbKuiTQCKtVi0PA5ZLqVmnkHPIqfPqDOGj/a3dvJVzGSgeijcIpjuEgFF54uirrWvIWmTBDeTA+tlQzJHpB2wQnUndd2gLDb8+eKFUZPBifshD3WmgWxv8wRv6k3DeWuWEZQ70Z+YDpgpeOzuzHj0MDBwMAlY8Qq86Rx6pxY76PLDDfHh3rE2CHJEKl2MhDj7pGXao2o633vSRd9ueG8W" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", + "salt": "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023", + "plaintext": "الكل في المجمو عة (5)", + "ciphertext": "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjTHmhdBC3meTY4A7Lv8s8B86MnmlUBJ8ebzwxFQzDyVCcdSbWFaKe0gigEBdXew7TjrjH8BCpAbtYjoa4YHa8GNjj7zH314ApVnwoByHdLHLB9Vr6VdzkxcJgA6oL4MAsRLg=" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", + "salt": "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0", + "plaintext": "𝖑𝖆𝖟𝖞 社會科學院語學研究所", + "ciphertext": "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLg/25Q5uBC88jl5ghtEREXX6o4QijPzM0uwmkeQ54/6aIqUyzGNVdryWKZ0mee2lmVVWhU+26X6XGFQ5DGRn+1v0POsFUCZ/REh35+beBNHnyvjxD/rbrMfhP2Blc8X5m8Xvk=" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", + "salt": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54", + "plaintext": "🙈 🙉 🙊 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗", + "ciphertext": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtU9bUC2ji2A2udRI2VCEQZ7IAmYRRgxodBtd5Yi/5htCUczf1jLHxIt9AhVAZLKuRgbWOuEMq5RBybkxPsSeAkxzXVOlWHZ1Febq5ogkjqY/6Xj8CwwmaZxfbx+d1BKKO3Wa+IFuXwuVAZa1Xo+fan+skyf+2R5QSj10QGAnGO7odAu/iZ9A28eMoSNeXsdxqy1+PRt5Zk4i019xmf7C4PDGSzgFZSvQ2EzusJN5WcsnRFmF1L5rXpX1AYo8HusOpWcGf9PjmFbO+8spUkX1W/T21GRm4o7dro1Y6ycgGOA9BsiQ==" } - ) == 0 - - if (isSuccessful) resultCipher else null + ], + "valid_pub": [ + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "0000000000000000000000000000000000000000000000000000000000000002", + "shared": "7a1ccf5ce5a08e380f590de0c02776623b85a61ae67cfb6a017317e505b7cb51", + "salt": "a000000000000000000000000000000000000000000000000000000000000001", + "plaintext": "⁰⁴⁵₀₁₂", + "ciphertext": "AqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2+xmGnjIMPMqqJGmjdYAYZUDUyEEUO3/evHUaO40LePeR91VlMVZ7I+nKJPkaUiKZ3cQiQnA86Uwti2IxepmzOFN", + "note": "sec1 = n-2, pub2: random, 0x02" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb", + "shared": "aa971537d741089885a0b48f2730a125e15b36033d089d4537a4e1204e76b39e", + "salt": "b000000000000000000000000000000000000000000000000000000000000002", + "plaintext": "A Peer-to-Peer Electronic Cash System", + "ciphertext": "ArAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACyuqG6RycuPyDPtwxzTcuMQu+is3N5XuWTlvCjligVaVBRydexaylXbsX592MEd3/Jt13BNL/GlpYpGDvLS4Tt/+2s9FX/16e/RDc+czdwXglc4DdSHiq+O06BvvXYfEQOPw=", + "note": "sec1 = 2, pub2: " + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "shared": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "salt": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "plaintext": "A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution. Digital signatures provide part of the solution, but the main benefits are lost if a trusted third party is still required to prevent double-spending.", + "ciphertext": "Anm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYb9wHyKevpe7ohJ6OkpceFcb0pySY8TLGwT7Q3zWNDKxc9blXanxKborEXkQH8xNaB2ViJfgxpkutbwbYd0Grix34xzaZBASufdsNm7R768t51tI6sdS0nms6kWLVJpEGu6Ke4Bldv4StJtWBLaTcgsgN+4WxDbBhC/nhwjEQiBBbbmUrPWjaVZXjl8dzzPrYtkSoeBNJs/UNvDwym4+qrmhv4ASTvVflpZgLlSe4seqeu6dWoRqn8uRHZQnPs+XhqwbdCHpeKGB3AfGBykZY0RIr0tjarWdXNasGbIhGM3GiLasioJeabAZw0plCevDkKpZYDaNfMJdzqFVJ8UXRIpvDpQad0SOm8lLum/aBzUpLqTjr3RvSlhYdbuODpd9pR5K60k4L2N8nrPtBv08wlilQg2ymwQgKVE6ipxIzzKMetn8+f0nQ9bHjWFJqxetSuMzzArTUQl9c4q/DwZmCBhI2", + "note": "sec1 == pub2 == salt" + } + ], + "invalid": [ + { + "sec1": "2573d1e9b9ac5de5d570f652cbb9e8d4f235e3d3d334181448e87c417f374e83", + "pub2": "8348c2d35549098706e5bab7966d9a9c72fbf6554e918f41c2b6cb275f79ec13", + "sharedKey": "8673ec68393a997bfad7eab8661461daf8b3931b7e885d78312a3fb7fe17f41a", + "salt": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "plaintext": "n o b l e", + "ciphertext": "##Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbOsrsqIEyf8ccwhlrnI/Cx03mDSmeweOLKD7dw5BDZQDxXe2FwUJ8Ag25VoJ4MGhjlPCNmCU/Uqk4k0jwbhgR3fRh", + "note": "unknown encryption version" + }, + { + "sec1": "11063318c5cb3cd9cafcced42b4db5ea02ec976ed995962d2bc1fa1e9b52e29f", + "pub2": "5c49873b6eac3dd363325250cc55d5dd4c7ce9a885134580405736d83506bb74", + "sharedKey": "e2aad10de00913088e5cb0f73fa526a6a17e95763cc5b2a127022f5ea5a73445", + "salt": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781", + "plaintext": "⚠️", + "ciphertext": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBA4fZmoHrtrz5I5pCHuwWZ22qqL/Xt1VidEZGMLds0yaJ5VwUbeEifEJlPICOFt1ssZJxCUf43HvRwCVTFskbhSMh", + "note": "unknown encryption version 0" + }, + { + "sec1": "2573d1e9b9ac5de5d570f652cbb9e8d4f235e3d3d334181448e87c417f374e83", + "pub2": "8348c2d35549098706e5bab7966d9a9c72fbf6554e918f41c2b6cb275f79ec13", + "sharedKey": "8673ec68393a997bfad7eab8661461daf8b3931b7e885d78312a3fb7fe17f41a", + "salt": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "plaintext": "n o s t r", + "ciphertext": "Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbOsrsqIEybscEwg5rnI/Cx03mDSmeweOLKD,7dw5BDZQDxXSlCwX1LIcTJEZaJPTz98Ftu0zSE0d93ED7OtdlvNeZx", + "note": "invalid base64" + }, + { + "sec1": "5a2f39347fed3883c9fe05868a8f6156a292c45f606bc610495fcc020ed158f7", + "pub2": "775bbfeba58d07f9d1fbb862e306ac780f39e5418043dadb547c7b5900245e71", + "sharedKey": "2e70c0a1cde884b88392458ca86148d859b273a5695ede5bbe41f731d7d88ffd", + "salt": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25", + "plaintext": "¯\\_(ツ)_/¯", + "ciphertext": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholdUejFZPARM22IvOqp1U/UmFSkeSyTBYbbwy5ykmi+mKiEcWL+nVmTOf28MMiC+rTpZys/8p1hqQFpn+XWZRPrVay", + "note": "invalid MAC" + }, + { + "sec1": "067eda13c4a36090ad28a7a183e9df611186ca01f63cb30fcdfa615ebfd6fb6d", + "pub2": "32c1ece2c5dd2160ad03b243f50eff12db605b86ac92da47eacc78144bf0cdd3", + "sharedKey": "a808915e31afc5b853d654d2519632dac7298ee2ecddc11695b8eba925935c2a", + "salt": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4", + "plaintext": "🥎", + "ciphertext": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0U0P/M1/oKm95z8qz6Kg0zQawLzwk3DskvWA2drXP4zK+tzHpKvWq0KOdx5MdypboSQsP4NXfhh2KoUffjkyIOiMA", + "note": "invalid MAC" + }, + { + "sec1": "3e7be560fb9f8c965c48953dbd00411d48577e200cf00d7cc427e49d0e8d9c01", + "pub2": "e539e5fee58a337307e2a937ee9a7561b45876fb5df405c5e7be3ee564b239cc", + "sharedKey": "6ee3efc4255e3b8270e5dd3f7dc7f6b60878cda6218c8df34a3261cd48744931", + "salt": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1", + "plaintext": "elliptic-curve cryptography", + "ciphertext": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHBu7F9m36yBd58mVUBB5ktBTOJREDaQT1KAyPmZidP+IRea1lNw5YAEK7+pbnpfCw8CD0i2n8Pf2IDWlKDhLiVvatw", + "note": "invalid padding" + }, + { + "sec1": "c22e1d4de967aa39dc143354d8f596cec1d7c912c3140831fff2976ce3e387c1", + "pub2": "4e405be192677a2da95ffc733950777213bf880cf7c3b084eeb6f3fe5bd43705", + "sharedKey": "1675a773dbf6fbcbef6a293004a4504b6c856978be738b10584b0269d437c8d1", + "salt": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330", + "plaintext": "Peer-to-Peer", + "ciphertext": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwhT0hvSnF9Xjp9Lml792qtNbmAVvR6laukTe9eYEjeWPpZFxtkVpYTbbL9wDKFeplDMKsUKVa+roSeSvv0ela9seDVl2Sfso=", + "note": "invalid padding" + }, + { + "sec1": "be1edab14c5912e5c59084f197f0945242e969c363096cccb59af8898815096f", + "pub2": "9eaf0775d971e4941c97189232542e1daefcdb7dddafc39bcea2520217710ba2", + "sharedKey": "1741a44c052d5ae363c7845441f73d2b6c28d9bfb3006190012bba12eb4c774b", + "salt": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b", + "plaintext": "censorship-resistant and global social network", + "ciphertext": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6bL9HUMo3Mjkz4bjQo/FJF2LWHmaCr9Byc3hU9D7we+EkNBWenBHasT1G52fZk9r3NKeOC1hLezNwBLr7XXiULh+NbMBDtJh9/aQh1uZ9EpAfeISOzbZXwYwf0P5M85g9XER8hZ2fgJDLb4qMOuQRG6CrPezhr357nS3UHwPC2qHo3uKACxhE+2td+965yDcvMTx4KYTQg1zNhd7PA5v/WPnWeq2B623yLxlevUuo/OvXplFho3QVy7s5QZVop6qV2g2/l/SIsvD0HIcv3V35sywOCBR0K4VHgduFqkx/LEF3NGgAbjONXQHX8ZKushsEeR4TxlFoRSovAyYjhWolz+Ok3KJL2Ertds3H+M/Bdl2WnZGT0IbjZjn3DS+b1Ke0R0X4Onww2ZG3+7o6ncIwTc+lh1O7YQn00V0HJ+EIp03heKV2zWdVSC615By/+Yt9KAiV56n5+02GAuNqA", + "note": "invalid padding" + } + ], + "invalid_conversation_key": [ + { + "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 higher than curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000000", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 is 0" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "note": "pub2 is invalid, no sqrt, all-ff" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 == curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "pub2 is invalid, no sqrt" + } + ], + "padding": [ + [16, 32], + [32, 32], + [33, 64], + [37, 64], + [45, 64], + [49, 64], + [64, 64], + [65, 96], + [100, 128], + [111, 128], + [200, 224], + [250, 256], + [320, 320], + [383, 384], + [384, 384], + [400, 448], + [500, 512], + [512, 512], + [515, 640], + [700, 768], + [800, 896], + [900, 1024], + [1020, 1024], + [74123, 81920] + ] } } - -enum class Nip44Version(val code: Int) { - Reserved(0), - XChaCha20(1) -} -``` - -# Test Vectors - -``` -{ - "valid_sec": [ - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000001", - "sec2": "0000000000000000000000000000000000000000000000000000000000000002", - "shared": "0135da2f8acf7b9e3090939432e47684eb888ea38c2173054d4eedffdf152ca5", - "nonce": "121f9d60726777642fd82286791ab4d7461c9502ebcbb6e6", - "plaintext": "a", - "ciphertext": "ARIfnWByZ3dkL9gihnkatNdGHJUC68u25qM=", - "note": "sk1 = 1, sk2 = random, 0x02" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000002", - "sec2": "0000000000000000000000000000000000000000000000000000000000000001", - "shared": "0135da2f8acf7b9e3090939432e47684eb888ea38c2173054d4eedffdf152ca5", - "plaintext": "a", - "ciphertext": "AeCt7jJ8L+WBOTiCSfeXEGXB/C/wgsrSRek=", - "nonce": "e0adee327c2fe58139388249f7971065c1fc2ff082cad245", - "note": "sk1 = 1, sk2 = random, 0x02" - } - ], - "valid_pub": [ - { - "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", - "pub2": "0000000000000000000000000000000000000000000000000000000000000002", - "shared": "a6d6a2f7011cdd1aeef325948f48c6efa40f0ec723ae7f5ac7e3889c43481500", - "nonce": "f481750e13dfa90b722b7cce0db39d80b0db2e895cc3001a", - "plaintext": "a", - "ciphertext": "AfSBdQ4T36kLcit8zg2znYCw2y6JXMMAGjM=", - "note": "sec1 = n-2, pub2: random, 0x02" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000002", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb", - "shared": "4908464f77dd74e11a9b4e4a3bc2467445bd794e8abcbfafb65a6874f9e25a8f", - "nonce": "45c484ba2c0397853183adba6922156e09a2ad4e3e6914f2", - "plaintext": "A Peer-to-Peer Electronic Cash System", - "ciphertext": "AUXEhLosA5eFMYOtumkiFW4Joq1OPmkU8k/25+3+VDFvOU39qkUDl1aiy8Q+0ozTwbhD57VJoIYayYS++hE=", - "note": "sec1 = 2, pub2: " - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000001", - "pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "shared": "132f39a98c31baaddba6525f5d43f2954472097fa15265f45130bfdb70e51def", - "nonce": "d60de08405cf9bde508147e82224ac6af409c12b9e5492e1", - "plaintext": "A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution. Digital signatures provide part of the solution, but the main benefits are lost if a trusted third party is still required to prevent double-spending.", - "ciphertext": "AdYN4IQFz5veUIFH6CIkrGr0CcErnlSS4VdvoQaP2DCB1dIFL72HSriG1aFABcTlu86hrsG0MdOO9rPdVXc3jptMMzqvIN6tJlHPC8GdwFD5Y8BT76xIIOTJR2W0IdrM7++WC/9harEJAdeWHDAC9zNJX81CpCz4fnV1FZ8GxGLC0nUF7NLeUiNYu5WFXQuO9uWMK0pC7tk3XVogk90X6rwq0MQG9ihT7e1elatDy2YGat+VgQlDrz8ZLRw/lvU+QqeXMQgjqn42sMTrimG6NdKfHJSVWkT6SKZYVsuTyU1Iu5Nk0twEV8d11/MPfsMx4i36arzTC9qxE6jftpOoG8f/jwPTSCEpHdZzrb/CHJcpc+zyOW9BZE2ZOmSxYHAE0ustC9zRNbMT3m6LqxIoHq8j+8Ysu+Cwqr4nUNLYq/Q31UMdDg1oamYS17mWIAS7uf2yF5uT5IlG", - "note": "sec1 == pub2" - } - ], - "invalid": [ - { - "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", - "note": "sec1 higher than curve.n" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000000", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", - "note": "sec1 is 0" - }, - { - "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", - "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "plaintext": "a", - "note": "pub2 is invalid, no sqrt, all-ff" - }, - { - "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", - "note": "sec1 == curve.n" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000002", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "plaintext": "a", - "note": "pub2 is invalid, no sqrt" - } - ] -} ``` diff --git a/46.md b/46.md index 7f4eb0f8f3..55d385cf9b 100644 --- a/46.md +++ b/46.md @@ -89,16 +89,15 @@ These are mandatory methods the remote signer app MUST implement: - params [`pubkey`, `nip4 ciphertext`] - result [`plaintext`] - **nip44_encrypt** - - params [`pubkey`, `plaintext`, `version`] + - params [`sender_privkey`, `recipient_pubkey`, `plaintext`, `version`] - result `nip44 encrypted payload` - **nip44_decrypt** - - params [`pubkey`, `nip44 encrypted payload`] + - params [`recipient_privkey`, `sender_pubkey`, `nip44 encrypted payload`] - result [`plaintext`] NOTICE: `pubkey` and `signature` are hex-encoded strings. - ### Nostr Connect URI **Signer** discovers **App** by scanning a QR code, clicking on a deep link or copy-pasting an URI. From 86e6f24676fb895526748f2f5e99c0923e7eb6ae Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Tue, 19 Dec 2023 23:34:51 +0300 Subject: [PATCH 3/3] Update spec. --- 44.md | 640 +++++++++++++++++++++++----------------------------------- 1 file changed, 254 insertions(+), 386 deletions(-) diff --git a/44.md b/44.md index 069b68e845..1282d261fb 100644 --- a/44.md +++ b/44.md @@ -1,428 +1,296 @@ -NIP-44 -====== +# NIP-44 -Encrypted Payloads (Versioned) ------------------------------- +## Encrypted Payloads (Versioned) `optional` `author:paulmillr` `author:staab` -The NIP introduces a new data format for keypair-based encryption. This NIP is versioned to allow multiple algorithm choices to exist simultaneously. +The NIP introduces a new data format for keypair-based encryption. This NIP is versioned +to allow multiple algorithm choices to exist simultaneously. -nostr is a key directory. Every nostr user has their own public key, -which solves key distribution problem, present in other solutions. -The main goal is to have at least *some* way to send messages between nostr accounts that cannot be read by everyone. +Nostr is a key directory. Every nostr user has their own public key, which solves key +distribution problems present in other solutions. The goal of this NIP is to have a +simple way to send messages between nostr accounts that cannot be read by everyone. -The scheme is not ideal, there are several points to consider: +The scheme has a number of important shortcomings: - No deniability: it is possible to prove the event was signed by a particular key - No forward secrecy: when a user key is compromised, it is possible to decrypt all previous conversations - No post-compromise security: when a user key is compromised, it is possible to decrypt all future conversations - No post-quantum security: a powerful quantum computer would be able to decrypt the messages -- Timing leak: Message date is public -- Location leak: User IP would be seen by relay and all intermediaries between user and relay -- Message length leak: Padding is obscuring true message length, but not entirely; attachments are not supported +- IP address leak: user IP may be seen by relays and all intermediaries between user and relay +- Date leak: the message date is public, since it is a part of NIP 01 event +- Limited message size leak: padding only partially obscures true message length +- No attachments: they are not supported -For risky situations, users should chat in specialized E2EE messaging software and limit nostr to exchanging contacts. +Lack of forward secrecy is partially mitigated: 1) the messages +should only be stored on relays, specified by the user, instead of a set of +all public relays 2) the relays are supposed to regularly delete older messages. -# Versions +For risky situations, users should chat in specialized E2EE messaging software and limit use +of nostr to exchanging contacts. + +## Dependence on NIP-01 + +It's not enough to use NIP-44 for encryption: the output must also be signed. + +In nostr case, the payload is serialized and signed as per NIP-01 rules. + +The same event can be serialized in two different ways, +resulting in two distinct signatures. So, it's important +to ensure serialization rules, which are defined in NIP-01, +are the same across different NIP-44 implementations. + +After serialization, the event is signed by Schnorr signature over secp256k1, +defined in BIP340. It's important to ensure the key and signature validity as +per BIP340 rules. + +## Versions Currently defined encryption algorithms: - `0x00` - Reserved -- `0x01` - Deprecated +- `0x01` - Deprecated and undefined - `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64 ## Version 2 -The following cryptographic primitives are used: +The algorithm choices are justified in a following way: + +- Encrypt-then-mac-then-sign instead of encrypt-then-sign-then-mac: + only events wrapped in NIP-01 signed envelope are currently accepted by nostr. +- ChaCha instead of AES: it's faster and has + [better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/) +- ChaCha instead of XChaCha: XChaCha has not been standardized. Also, we don't need xchacha's improved + collision resistance of nonces: every message has a new (key, nonce) pair. +- HMAC-SHA256 instead of Poly1305: polynomial MACs are much easier to forge +- SHA256 instead of SHA3 or BLAKE: it is already used in nostr. Also blake's + speed advantage is smaller in non-parallel environments +- Custom padding instead of padmé: better leakage reduction for small messages +- Base64 encoding instead of an other compression algorithm: it is widely available, + and is already used in nostr + +### Functions and operations + +- Cryptographic methods + - `secure_random_bytes(length)` fetches randomness from CSPRNG + - `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869) with SHA256 hash function, + comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)` + - `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439), with starting counter set to 0 + - `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104) + - `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by + scalar a (`a ⋅ B`), defined in + [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki). + The operation produces shared point, and we encode the shared point's 32-byte x coordinate, + using method `bytes(P)` from BIP340. Private and public keys must be validated + as per BIP340: pubkey must be a valid, on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]` +- Operators + - `x[i:j]`, where `x` is a byte array and `i, j <= 0`, + returns a `(j - i)`-byte array with a copy of the `i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x` +- Constants `c`: + - `min_plaintext_size` is 1. 1b msg is padded to 32b. + - `max_plaintext_size` is 65535 (64kb - 1). It is padded to 65536. +- Functions + - `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding) + - `concat` refers to byte array concatenation + - `is_equal_ct(a, b)` is constant-time equality check of 2 byte arrays + - `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back + - `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array + - `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array + - `zeros(length)` creates byte array of length `length >= 0`, filled with zeros + - `floor(number)` and `log2(number)` are well-known mathematical methods + +User-defined functions: ```py -chacha20(key, nonce, data) # ChaCha20 (RFC 8439) -hmac_sha256(key, message) # HMAC (RFC 2104) -hkdf_sha256(key, salt, info, length) # HKDF (RFC 5869) -secp256k1_ecdh(privA, pubB) # ECDH over secp256k1 curve, **32-byte output is not hashed** -base64_encode(string), base64_decode(bytes) # Base64 (RFC 4648, with padding) -secure_random_bytes(length) # Fetches randomness from CSPRNG -``` - -Helper methods: - -```py -# Calculates static long-term key between users A and B. -# key(Apriv, Bpub) == key(Bpriv, Apub) -# Make sure to validate public keys. -# The method is slow, so, the output could be cached. -# Returns 32 bytes representing unhashed x coordinate. -get_conversation_key(private_key_a, public_key_b): - secp256k1_ecdh(private_key_a, public_key_b).slice(1, 33) - -# Calculates encryption and authentication keys for a message. -get_message_keys(conversation_key, salt): - res = hkdf_sha256(conversation_key, salt: salt, info: 'nip44-v2', length: 76) - { "encryption": res.slice(0, 32), "nonce": res.slice(32, 44), "auth": res.slice(44, 76) } - # Calculates length of the padded byte array. -calc_padding(unpadded_len): - nextpower = 1 << (floor(log2(unpadded_len - 1)) + 1 - chunk = nextpower <= 256 ? 32 : nextpower / 8 - padded_length = unpadded_len <= 32 ? 32 : chunk * (floor((len - 1) / chunk) + 1) - -# Language and platform-specific methods, we don't define them in the spec -utf8_encode(string), utf8_decode(bytes) # Transforms string to byte array and back -write_u16_be(number) # Encodes number into Big-Endian uint16 byte array -zeros(length) # Creates byte array of length "length", filled with zeros. Can be 0-length +def calc_padded_len(unpadded_len): + next_power = 1 << (floor(log2(unpadded_len - 1))) + 1 + if next_power <= 256: + chunk = 32 + else: + chunk = next_power / 8 + if unpadded_len <= 32: + return 32 + else: + return chunk * (floor((len - 1) / chunk) + 1) + +# Converts unpadded plaintext to padded bytearray +def pad(plaintext): + unpadded = utf8_encode(plaintext) + unpadded_len = len(plaintext) + if (unpadded_len < c.min_plaintext_size or + unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length') + prefix = write_u16_be(unpadded_len) + suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len) + return concat(prefix, unpadded, suffix) + +# Converts padded bytearray to unpadded plaintext +def unpad(padded): + unpadded_len = read_uint16_be(padded[0:2]) + unpadded = padded[2:2+unpadded_len] + if (unpadded_len == 0 or + len(unpadded) != unpadded_len or + len(padded) != 2 + calc_padded_len(unpadded_len)): raise Exception('invalid padding') + return utf8_decode(unpadded) + +# metadata: always 65b (version: 1b, nonce: 32b, max: 32b) +# plaintext: 1b to 0xffff +# padded plaintext: 32b to 0xffff +# ciphertext: 32b+2 to 0xffff+2 +# raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) +# compressed payload (base64): 132b to 87472b +def decode_payload(payload): + plen = len(payload) + if plen == 0 or payload[0] == '#': raise Exception('unknown version') + if plen < 132 or plen > 87472: raise Exception('invalid payload size') + data = base64_decode(payload) + dlen = len(d) + if dlen < 99 or dlen > 65603: raise Exception('invalid data size'); + vers = data[0] + if vers != 2: raise Exception('unknown version ' + vers) + nonce = data[1:33] + ciphertext = data[33:dlen - 32] + mac = data[dlen - 32:dlen] + return (nonce, ciphertext, mac) + +def hmac_aad(key, message, aad): + if len(aad) != 32: raise Exception('AAD associated data must be 32 bytes'); + return hmac(sha256, key, concat(aad, message)); + +# Calculates long-term key between users A and B: `get_key(Apriv, Bpub) == get_key(Bpriv, Apub)` +def get_conversation_key(private_key_a, public_key_b): + shared_x = secp256k1_ecdh(private_key_a, public_key_b) + return hkdf_extract(IKM=shared_x, salt=utf8_encode('nip44-v2')) + +# Calculates unique per-message key +def get_message_keys(conversation_key, nonce): + if len(conversation_key) != 32: raise Exception('invalid conversation_key length') + if len(nonce) != 32: raise Exception('invalid nonce length') + keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76) + chacha_key = keys[0:32] + chacha_nonce = keys[32:44] + hmac_key = keys[44:76] + return (chacha_key, chacha_nonce, hmac_key) + +def encrypt(plaintext, conversation_key, nonce): + (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce) + padded = pad(plaintext) + ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded) + mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce) + return base64_encode(concat(write_u8(2), nonce, ciphertext, mac)) + +def decrypt(payload, conversation_key): + (nonce, ciphertext, mac) = decode_payload(payload) + (chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce) + calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce) + if not is_equal_ct(calculated_mac, mac): raise Exception('invalid MAC') + padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext) + return unpad(padded_plaintext) + +# Usage: +# conversation_key = get_conversation_key(sender_privkey, recipient_pubkey) +# nonce = secure_random_bytes(32) +# payload = encrypt('hello world', conversation_key, nonce) +# 'hello world' == decrypt(payload, conversation_key) ``` #### Encryption -```py -# input: `sender_privkey, recipient_pubkey, plaintext` OR `conversation_key, plaintext` -salt = secure_random_bytes(32) -conversation_key = get_conversation_key(sender_privkey, recipient_pubkey) -keys = get_message_keys(conversation_key, salt) -unpadded = utf8_encode(plaintext) -unpadded_len = plaintext.length -if (unpadded_len == 0 or unpadded_len > 65536 - 128): throw Error('invalid plaintext length') -padded = write_u16_be(unpadded_len) || unpadded || zeros(calc_padding(unpadded_len) - unpadded_len) -ciphertext = chacha20(key: keys["encryption"], nonce: keys["nonce"], data: padded) -mac = hmac_sha256(key: keys["auth"], message: ciphertext) -payload = base64_encode(2 || salt || ciphertext || mac) -``` - -1. Generate random 32-byte `salt` - * Secure random (CSPRNG) must always be used for such generation - * Reusing `salt` between messages would make them decryptable, but would not leak long-term key -2. Calculate message keys - * Generate `conversation_key`: take 32 bytes from unhashed secp256k1 ECDH of private key A and public key B, without parity byte - * Pass `conversation_key` to hkdf-sha256, with salt from step 1 and info: `nip44-v2`. HKDF's outputLength must be 76 bytes. - * The HKDF output is sliced into: `encryption_key` (bytes 0..32), `nonce` (bytes 32..44), `auth` (bytes 44..76) -3. Encode plaintext from utf8 to bytes and pad them - * Validate plaintext length. Minimum is 1 byte, maximum is 65536 - 128 bytes - * Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]` - * Padding algorithm is related to powers-of-two, with min padded msg size of 32 - * Plaintext length is encoded in big-endian as first two bytes of the padded blob - * In some cases, there is no padding: for example, 320-byte msg is padded to 320 bytes - * Padding test vectors are provided below: ensure your padding calculator output matches them -5. Produce ciphertext, by calling ChaCha20 -6. Calculate MAC (message authentication code) over ciphertext -7. Base64-encode the version and concatenated params +1. Calculate conversation key + - Execute ECDH (scalar multiplication) of public key B by private key A. + Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point. + - Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')` + - HKDF output will be `conversation_key` between two users + - It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)` +2. Generate random 32-byte nonce + - Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) + - Don't generate nonce from message content + - Don't re-use the same nonce between messages: doing so would make them decryptable, + but won't leak long-term key +3. Calculate message keys + - The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes + - Use HKDF-expand, with sha256, `OKM=conversation_key`, `info=nonce` and `L=76` + - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76) +4. Add padding + - Content must be encoded from UTF-8 into byte array + - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes + - Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]` + - Padding algorithm is related to powers-of-two, with min padded msg size of 32 + - Plaintext length is encoded in big-endian as first 2 bytes of the padded blob +5. Encrypt padded content + - Use ChaCha20, with key and nonce from step 3 +6. Calculate MAC (message authentication code) with AAD + - AAD is used: instead of calculating MAC on ciphertext, + it's calculated over a concatenation of `nonce` and `ciphertext` + - Validate that AAD (nonce) is 32 bytes +7. Base64-encode (with padding) params: `concat(version, nonce, ciphertext, mac)` + +After encryption, it's necessary to sign it. Use NIP-01 to serialize the event, +with result base64 assigned to event's `content`. Then, use NIP-01 to sign +the event using schnorr signature scheme over secp256k1. #### Decryption -Before decryption, it is mandatory to validate event's pubkey and signature: `validate_public_key(event.pubkey) and validate_signature(event)` - -```py -# input: `recipient_privkey, sender_pubkey, payload` OR `conversation_key, payload` -if payload[0] == '#': throw Error('unknown version') -d = base64_decode(payload) -vers = d[0] -if (vers != 2): throw Error('unknown version') - -dlen = d.length -salt, ciphertext, mac = d.slice(1, 33), d.slice(33, dlen - 32), d.slice(dlen - 32, dlen) -conversation_key = get_conversation_key(sender_privkey, recipient_pubkey) -keys = get_message_keys(conversation_key, salt) -calculated_mac = hmac_sha256(key: keys["auth"], message: ciphertext) -if (calculated_mac != mac): throw Error('invalid MAC') - -padded = chacha20(key: keys["encryption"], nonce: keys["nonce"], data: ciphertext) -unpadded_len = read_uint16_be(padded.slice(0, 2)) -unpadded = padded_plaintext.slice(2, 2 + unpadded_len) -if (unpadded_len == 0 or - unpadded.length != unpadded_len or - padded.length != 2 + calc_padding(unpadded_len)): throw Error('invalid padding') -plaintext = utf8_decode(unpadded) -``` - -1. Validate the message's pubkey and signature - * public key must be a valid secp256k1 curve point - * signature must be valid secp256k1 schnorr signature; message serialization is specified in NIP1 -2. Check if first payload's character is `#`. Throw a descriptive error if so - * `#` is an optional flag that means non-base64 encoding is used - * Instead of throwing `base64 is invalid`, an app must say the encryption version is not yet supported -3. Decode base64 - * Base64 is decoded into `version, salt, ciphertext, mac` - * If the version is unknown, the app, an app must say the encryption version is not yet supported +Before decryption, it's necessary to validate the message's pubkey and signature. +The public key must be a valid non-zero secp256k1 curve point, and signature must be valid +secp256k1 schnorr signature. For exact validation rules, refer to BIP-340. + +1. Check if first payload's character is `#` + - `#` is an optional future-proof flag that means non-base64 encoding is used + - The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`, + an app must say the encryption version is not yet supported +2. Decode base64 + - Base64 is decoded into `version, nonce, ciphertext, mac` + - If the version is unknown, the app, an app must say the encryption version is not yet supported + - Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars + - Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes +3. Calculate conversation key + - See step 1 of Encryption 4. Calculate message keys - * Reuse algorithm from encryption's step 2 -5. Calculate and compare MAC using `auth_key` from step 4, reusing the algorithm from encryption's step 5 - * Stop and throw an error if MAC doesn't match the decoded one from step 2 -6. Decrypt ciphertext into plaintext, using ChaCha20 -7. Unpad plaintext - * Read the first two BE bytes of plaintext that correspond to plaintext length - * Verify that the length of sliced plaintext matches the value of the two BE bytes - * Verify that calculated padding from encryption's step 3 matches the actual padding - -Example: + - See step 3 of Encryption +5. Calculate MAC (message authentication code) with AAD and compare + - Stop and throw an error if MAC doesn't match the decoded one from step 2 + - Use constant-time comparison algorithm +6. Decrypt ciphertext + - Use ChaCha20 with key and nonce from step 3 +7. Remove padding + - Read the first two BE bytes of plaintext that correspond to plaintext length + - Verify that the length of sliced plaintext matches the value of the two BE bytes + - Verify that calculated padding from encryption's step 3 matches the actual padding -- Alice's private key: `5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a` -- Bob's private key: `4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d` -- Message salt: `b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b` +## Tests and code -Encrypting the message `hello` from Alice to Bob results in the following base-64 encoded payload: +A collection of implementations in different languages is +available [on GitHub](https://github.com/paulmillr/nip44). -``` -ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7ysu7m8bzLLv3LyxbtMit2SsnmvFjnrJN9Qqoenb/M2mwWjcfA92Xeb92ZrTKcaQOi6jdXajWgRcRxO/TWJo93il3 -``` - -# Implementations +We publish extensive test vectors. Instead of having it in the +document directly, a sha256 checksum of vectors is provided: -A collection of implementations in different languages is available [on GitHub](https://github.com/paulmillr/nip44-implementations). + 269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json -# Test Vectors - -Steps that must be tested: - -- `valid_sec` - encrypt (calculate and compare shared key, calculate and compare ciphertext), decrypt (compare plaintext) -- `valid_pub` - encrypt (calculate and compare shared key, calculate and compare ciphertext), decrypt (compare plaintext) -- `invalid` - decrypt must throw an error -- `invalid_conversation_key` - encrypt or get_conversation_key must throw an error -- `padding` - tests for calc_padding utility method +Example of test vector from the file: ```json { - "v2": { - "valid_sec": [ - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000001", - "sec2": "0000000000000000000000000000000000000000000000000000000000000002", - "shared": "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", - "salt": "0000000000000000000000000000000000000000000000000000000000000001", - "plaintext": "a", - "ciphertext": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYNpT9ESckRbRUY7bUF5P+1rObpA4BNoksAUQ8myMDd9/37W/J2YHvBpRjvy9uC0+ovbpLc0WLaMFieqAMdIYqR14", - "note": "sk1 = 1, sk2 = random, 0x02" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000002", - "sec2": "0000000000000000000000000000000000000000000000000000000000000001", - "shared": "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", - "salt": "f00000000000000000000000000000f00000000000000000000000000000000f", - "plaintext": "🍕🫃", - "ciphertext": "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPKY68BwdF7PIT205jBoaZHSs7OMpKsULW5F5ClOJWiy6XjZy7s2v85KugYmbBKgEC2LytbXbxkr7Jpgfk529K3/pP", - "note": "sk1 = 1, sk2 = random, 0x02" - }, - { - "sec1": "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a", - "sec2": "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d", - "shared": "94da47d851b9c1ed33b3b72f35434f56aa608d60e573e9c295f568011f4f50a4", - "salt": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b", - "plaintext": "表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀", - "ciphertext": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7yuU7XwJ8wCYUrq4aXX86HLnkMx7fPFvNeMk0uek9ma01magfEBIf+vJvZdWKiv48eUu9Cv31plAJsH6kSIsGc5TVYBYipkrQUNRxxJA15QT+uCURF96v3XuSS0k2Pf108AI=", - "note": "unicode-heavy string" - }, - { - "sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c", - "sec2": "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba", - "shared": "ab99c122d4586cdd5c813058aa543d0e7233545dbf6874fc34a3d8d9a18fbbc3", - "salt": "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf", - "plaintext": "ability🤝的 ȺȾ", - "ciphertext": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvPSc+7YCIFTmGk5OLuh1nhl6TvID7sGKLFUCWRW1eRfV/0a7sT46N3nTQzD7IE67zLWrYqGnE+0DDNz6sJ4hAaFrT" - }, - { - "sec1": "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c", - "sec2": "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae", - "shared": "a449f2a85c6d3db0f44c64554a05d11a3c0988d645e4b4b2592072f63662f422", - "salt": "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1", - "plaintext": "pepper👀їжак", - "ciphertext": "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGx1TkkMfiHJxEeSdQ/4Rlaghn0okDCNYLihBsHrDzBsNRC27APmH9mmZcpcg66Mb0exH9V5/lLBWdQW+fcY9GpvXv0" - }, - { - "sec1": "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f", - "sec2": "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3", - "shared": "decde9938ffcb14fa7ff300105eb1bf239469af9baf376e69755b9070ae48c47", - "salt": "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407", - "plaintext": "( ͡° ͜ʖ ͡°)", - "ciphertext": "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHiR8Hljs6Nl/XsNDAmCz6U1Z3NUGhbCtczc3wXXxDzFkjjMimxsf/74OEzu7LphUadM9iSWvVKPrNXY7lTD0B2muz" - }, - { - "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", - "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", - "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", - "salt": "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68", - "plaintext": "مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،", - "ciphertext": "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItohfde4vHVRHUupr+Glh9JW4f9EY+w795hvRZbixs0EQgDZ7zwLlymVQI3NNvMqvemQzHUA1I5+9gSu8XSMwX9gDCUAjUJtntCkRt9+tjdy2Wa2ZrDYqCvgirvzbJTIC69Ve3YbKuiTQCKtVi0PA5ZLqVmnkHPIqfPqDOGj/a3dvJVzGSgeijcIpjuEgFF54uirrWvIWmTBDeTA+tlQzJHpB2wQnUndd2gLDb8+eKFUZPBifshD3WmgWxv8wRv6k3DeWuWEZQ70Z+YDpgpeOzuzHj0MDBwMAlY8Qq86Rx6pxY76PLDDfHh3rE2CHJEKl2MhDj7pGXao2o633vSRd9ueG8W" - }, - { - "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", - "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", - "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", - "salt": "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023", - "plaintext": "الكل في المجمو عة (5)", - "ciphertext": "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjTHmhdBC3meTY4A7Lv8s8B86MnmlUBJ8ebzwxFQzDyVCcdSbWFaKe0gigEBdXew7TjrjH8BCpAbtYjoa4YHa8GNjj7zH314ApVnwoByHdLHLB9Vr6VdzkxcJgA6oL4MAsRLg=" - }, - { - "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", - "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", - "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", - "salt": "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0", - "plaintext": "𝖑𝖆𝖟𝖞 社會科學院語學研究所", - "ciphertext": "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLg/25Q5uBC88jl5ghtEREXX6o4QijPzM0uwmkeQ54/6aIqUyzGNVdryWKZ0mee2lmVVWhU+26X6XGFQ5DGRn+1v0POsFUCZ/REh35+beBNHnyvjxD/rbrMfhP2Blc8X5m8Xvk=" - }, - { - "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", - "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", - "shared": "c6f2fde7aa00208c388f506455c31c3fa07caf8b516d43bf7514ee19edcda994", - "salt": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54", - "plaintext": "🙈 🙉 🙊 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗", - "ciphertext": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtU9bUC2ji2A2udRI2VCEQZ7IAmYRRgxodBtd5Yi/5htCUczf1jLHxIt9AhVAZLKuRgbWOuEMq5RBybkxPsSeAkxzXVOlWHZ1Febq5ogkjqY/6Xj8CwwmaZxfbx+d1BKKO3Wa+IFuXwuVAZa1Xo+fan+skyf+2R5QSj10QGAnGO7odAu/iZ9A28eMoSNeXsdxqy1+PRt5Zk4i019xmf7C4PDGSzgFZSvQ2EzusJN5WcsnRFmF1L5rXpX1AYo8HusOpWcGf9PjmFbO+8spUkX1W/T21GRm4o7dro1Y6ycgGOA9BsiQ==" - } - ], - "valid_pub": [ - { - "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", - "pub2": "0000000000000000000000000000000000000000000000000000000000000002", - "shared": "7a1ccf5ce5a08e380f590de0c02776623b85a61ae67cfb6a017317e505b7cb51", - "salt": "a000000000000000000000000000000000000000000000000000000000000001", - "plaintext": "⁰⁴⁵₀₁₂", - "ciphertext": "AqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2+xmGnjIMPMqqJGmjdYAYZUDUyEEUO3/evHUaO40LePeR91VlMVZ7I+nKJPkaUiKZ3cQiQnA86Uwti2IxepmzOFN", - "note": "sec1 = n-2, pub2: random, 0x02" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000002", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb", - "shared": "aa971537d741089885a0b48f2730a125e15b36033d089d4537a4e1204e76b39e", - "salt": "b000000000000000000000000000000000000000000000000000000000000002", - "plaintext": "A Peer-to-Peer Electronic Cash System", - "ciphertext": "ArAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACyuqG6RycuPyDPtwxzTcuMQu+is3N5XuWTlvCjligVaVBRydexaylXbsX592MEd3/Jt13BNL/GlpYpGDvLS4Tt/+2s9FX/16e/RDc+czdwXglc4DdSHiq+O06BvvXYfEQOPw=", - "note": "sec1 = 2, pub2: " - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000001", - "pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "shared": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "salt": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "plaintext": "A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution. Digital signatures provide part of the solution, but the main benefits are lost if a trusted third party is still required to prevent double-spending.", - "ciphertext": "Anm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYb9wHyKevpe7ohJ6OkpceFcb0pySY8TLGwT7Q3zWNDKxc9blXanxKborEXkQH8xNaB2ViJfgxpkutbwbYd0Grix34xzaZBASufdsNm7R768t51tI6sdS0nms6kWLVJpEGu6Ke4Bldv4StJtWBLaTcgsgN+4WxDbBhC/nhwjEQiBBbbmUrPWjaVZXjl8dzzPrYtkSoeBNJs/UNvDwym4+qrmhv4ASTvVflpZgLlSe4seqeu6dWoRqn8uRHZQnPs+XhqwbdCHpeKGB3AfGBykZY0RIr0tjarWdXNasGbIhGM3GiLasioJeabAZw0plCevDkKpZYDaNfMJdzqFVJ8UXRIpvDpQad0SOm8lLum/aBzUpLqTjr3RvSlhYdbuODpd9pR5K60k4L2N8nrPtBv08wlilQg2ymwQgKVE6ipxIzzKMetn8+f0nQ9bHjWFJqxetSuMzzArTUQl9c4q/DwZmCBhI2", - "note": "sec1 == pub2 == salt" - } - ], - "invalid": [ - { - "sec1": "2573d1e9b9ac5de5d570f652cbb9e8d4f235e3d3d334181448e87c417f374e83", - "pub2": "8348c2d35549098706e5bab7966d9a9c72fbf6554e918f41c2b6cb275f79ec13", - "sharedKey": "8673ec68393a997bfad7eab8661461daf8b3931b7e885d78312a3fb7fe17f41a", - "salt": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", - "plaintext": "n o b l e", - "ciphertext": "##Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbOsrsqIEyf8ccwhlrnI/Cx03mDSmeweOLKD7dw5BDZQDxXe2FwUJ8Ag25VoJ4MGhjlPCNmCU/Uqk4k0jwbhgR3fRh", - "note": "unknown encryption version" - }, - { - "sec1": "11063318c5cb3cd9cafcced42b4db5ea02ec976ed995962d2bc1fa1e9b52e29f", - "pub2": "5c49873b6eac3dd363325250cc55d5dd4c7ce9a885134580405736d83506bb74", - "sharedKey": "e2aad10de00913088e5cb0f73fa526a6a17e95763cc5b2a127022f5ea5a73445", - "salt": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781", - "plaintext": "⚠️", - "ciphertext": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBA4fZmoHrtrz5I5pCHuwWZ22qqL/Xt1VidEZGMLds0yaJ5VwUbeEifEJlPICOFt1ssZJxCUf43HvRwCVTFskbhSMh", - "note": "unknown encryption version 0" - }, - { - "sec1": "2573d1e9b9ac5de5d570f652cbb9e8d4f235e3d3d334181448e87c417f374e83", - "pub2": "8348c2d35549098706e5bab7966d9a9c72fbf6554e918f41c2b6cb275f79ec13", - "sharedKey": "8673ec68393a997bfad7eab8661461daf8b3931b7e885d78312a3fb7fe17f41a", - "salt": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", - "plaintext": "n o s t r", - "ciphertext": "Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbOsrsqIEybscEwg5rnI/Cx03mDSmeweOLKD,7dw5BDZQDxXSlCwX1LIcTJEZaJPTz98Ftu0zSE0d93ED7OtdlvNeZx", - "note": "invalid base64" - }, - { - "sec1": "5a2f39347fed3883c9fe05868a8f6156a292c45f606bc610495fcc020ed158f7", - "pub2": "775bbfeba58d07f9d1fbb862e306ac780f39e5418043dadb547c7b5900245e71", - "sharedKey": "2e70c0a1cde884b88392458ca86148d859b273a5695ede5bbe41f731d7d88ffd", - "salt": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25", - "plaintext": "¯\\_(ツ)_/¯", - "ciphertext": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholdUejFZPARM22IvOqp1U/UmFSkeSyTBYbbwy5ykmi+mKiEcWL+nVmTOf28MMiC+rTpZys/8p1hqQFpn+XWZRPrVay", - "note": "invalid MAC" - }, - { - "sec1": "067eda13c4a36090ad28a7a183e9df611186ca01f63cb30fcdfa615ebfd6fb6d", - "pub2": "32c1ece2c5dd2160ad03b243f50eff12db605b86ac92da47eacc78144bf0cdd3", - "sharedKey": "a808915e31afc5b853d654d2519632dac7298ee2ecddc11695b8eba925935c2a", - "salt": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4", - "plaintext": "🥎", - "ciphertext": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0U0P/M1/oKm95z8qz6Kg0zQawLzwk3DskvWA2drXP4zK+tzHpKvWq0KOdx5MdypboSQsP4NXfhh2KoUffjkyIOiMA", - "note": "invalid MAC" - }, - { - "sec1": "3e7be560fb9f8c965c48953dbd00411d48577e200cf00d7cc427e49d0e8d9c01", - "pub2": "e539e5fee58a337307e2a937ee9a7561b45876fb5df405c5e7be3ee564b239cc", - "sharedKey": "6ee3efc4255e3b8270e5dd3f7dc7f6b60878cda6218c8df34a3261cd48744931", - "salt": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1", - "plaintext": "elliptic-curve cryptography", - "ciphertext": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHBu7F9m36yBd58mVUBB5ktBTOJREDaQT1KAyPmZidP+IRea1lNw5YAEK7+pbnpfCw8CD0i2n8Pf2IDWlKDhLiVvatw", - "note": "invalid padding" - }, - { - "sec1": "c22e1d4de967aa39dc143354d8f596cec1d7c912c3140831fff2976ce3e387c1", - "pub2": "4e405be192677a2da95ffc733950777213bf880cf7c3b084eeb6f3fe5bd43705", - "sharedKey": "1675a773dbf6fbcbef6a293004a4504b6c856978be738b10584b0269d437c8d1", - "salt": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330", - "plaintext": "Peer-to-Peer", - "ciphertext": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwhT0hvSnF9Xjp9Lml792qtNbmAVvR6laukTe9eYEjeWPpZFxtkVpYTbbL9wDKFeplDMKsUKVa+roSeSvv0ela9seDVl2Sfso=", - "note": "invalid padding" - }, - { - "sec1": "be1edab14c5912e5c59084f197f0945242e969c363096cccb59af8898815096f", - "pub2": "9eaf0775d971e4941c97189232542e1daefcdb7dddafc39bcea2520217710ba2", - "sharedKey": "1741a44c052d5ae363c7845441f73d2b6c28d9bfb3006190012bba12eb4c774b", - "salt": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b", - "plaintext": "censorship-resistant and global social network", - "ciphertext": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6bL9HUMo3Mjkz4bjQo/FJF2LWHmaCr9Byc3hU9D7we+EkNBWenBHasT1G52fZk9r3NKeOC1hLezNwBLr7XXiULh+NbMBDtJh9/aQh1uZ9EpAfeISOzbZXwYwf0P5M85g9XER8hZ2fgJDLb4qMOuQRG6CrPezhr357nS3UHwPC2qHo3uKACxhE+2td+965yDcvMTx4KYTQg1zNhd7PA5v/WPnWeq2B623yLxlevUuo/OvXplFho3QVy7s5QZVop6qV2g2/l/SIsvD0HIcv3V35sywOCBR0K4VHgduFqkx/LEF3NGgAbjONXQHX8ZKushsEeR4TxlFoRSovAyYjhWolz+Ok3KJL2Ertds3H+M/Bdl2WnZGT0IbjZjn3DS+b1Ke0R0X4Onww2ZG3+7o6ncIwTc+lh1O7YQn00V0HJ+EIp03heKV2zWdVSC615By/+Yt9KAiV56n5+02GAuNqA", - "note": "invalid padding" - } - ], - "invalid_conversation_key": [ - { - "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "note": "sec1 higher than curve.n" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000000", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "note": "sec1 is 0" - }, - { - "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", - "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "note": "pub2 is invalid, no sqrt, all-ff" - }, - { - "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "note": "sec1 == curve.n" - }, - { - "sec1": "0000000000000000000000000000000000000000000000000000000000000002", - "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "note": "pub2 is invalid, no sqrt" - } - ], - "padding": [ - [16, 32], - [32, 32], - [33, 64], - [37, 64], - [45, 64], - [49, 64], - [64, 64], - [65, 96], - [100, 128], - [111, 128], - [200, 224], - [250, 256], - [320, 320], - [383, 384], - [384, 384], - [400, 448], - [500, 512], - [512, 512], - [515, 640], - [700, 768], - [800, 896], - [900, 1024], - [1020, 1024], - [74123, 81920] - ] - } + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "sec2": "0000000000000000000000000000000000000000000000000000000000000002", + "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", + "nonce": "0000000000000000000000000000000000000000000000000000000000000001", + "plaintext": "a", + "payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb" } ``` + +The file also contains intermediate values. A quick guidance with regards to its usage: + +- `valid.get_conversation_key`: calculate conversation_key from secret key sec1 and public key pub2 +- `valid.get_message_keys`: calculate chacha_key, chacha_nocne, hmac_key from conversation_key and nonce +- `valid.calc_padded_len`: take unpadded length (first value), calculate padded length (second value) +- `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. +- `valid.encrypt_decrypt_long_msg`: same as previous step, but instead of a full plaintext and payload, + their checksum is provided. +- `invalid.encrypt_msg_lengths` +- `invalid.get_conversation_key`: calculating converastion_key must throw an error +- `invalid.decrypt`: decrypting message content must throw an error