nostril

A C cli tool for creating nostr events
git clone git://jb55.com/nostril
Log | Files | Refs | Submodules | README | LICENSE

nip44.c (13152B)


      1 
      2 #include "base64.h"
      3 #include "secp256k1.h"
      4 #include "secp256k1_ecdh.h"
      5 #include "secp256k1_schnorrsig.h"
      6 #include "hmac_sha256.h"
      7 #include "hkdf_sha256.h"
      8 #include "nip44.h"
      9 #include "random.h"
     10 #include "cursor.h"
     11 #include "sodium/crypto_stream_chacha20.h"
     12 #include <string.h>
     13 
     14 /* NIP44 payload encryption/decryption */
     15 
     16 static int copyx(unsigned char *output, const unsigned char *x32,
     17 		 const unsigned char *y32, void *data)
     18 {
     19 	memcpy(output, x32, 32);
     20 	return 1;
     21 }
     22 
     23 static enum ndb_decrypt_result
     24 calculate_shared_secret(secp256k1_context *ctx,
     25 			const unsigned char *seckey,
     26 			const unsigned char *pubkey,
     27 			unsigned char *shared_secret)
     28 {
     29 	secp256k1_pubkey parsed_pubkey;
     30 	unsigned char compressed_pubkey[33];
     31 	compressed_pubkey[0] = 2;
     32 	memcpy(&compressed_pubkey[1], pubkey, 32);
     33 
     34         if (!secp256k1_ec_seckey_verify(ctx, seckey)) {
     35 		return NIP44_ERR_SECKEY_VERIFY_FAILED;
     36 	}
     37 
     38 	if (!secp256k1_ec_pubkey_parse(ctx, &parsed_pubkey, compressed_pubkey, sizeof(compressed_pubkey))) {
     39 		return NIP44_ERR_PUBKEY_PARSE_FAILED;
     40 	}
     41 
     42 	if (!secp256k1_ecdh(ctx, shared_secret, &parsed_pubkey, seckey, copyx, NULL)) {
     43 		return NIP44_ERR_ECDH_FAILED;
     44 	}
     45 
     46 	return NIP44_OK;
     47 }
     48 
     49 struct message_keys {
     50 	unsigned char key[32];
     51 	unsigned char nonce[12];
     52 	unsigned char auth[32];
     53 };
     54 
     55 static void hmac_aad(struct hmac_sha256 *out,
     56 		     unsigned char hmac[32], unsigned char *aad,
     57 		     const unsigned char *msg, size_t msgsize)
     58 {
     59 	struct hmac_sha256_ctx ctx;
     60 	hmac_sha256_init(&ctx, hmac, 32);
     61 	hmac_sha256_update(&ctx, aad, 32);
     62 	hmac_sha256_update(&ctx, msg, msgsize);
     63 	hmac_sha256_done(&ctx, out);
     64 }
     65 
     66 enum ndb_decrypt_result
     67 nip44_decode_payload(struct nip44_payload *decoded,
     68 		     unsigned char *buf, size_t bufsize,
     69 		     const char *payload, size_t payload_len)
     70 {
     71 	size_t decoded_len;
     72 
     73 	/* NOTE(jb55): we use the variant that doesn't have an
     74 	 *             upper size limit
     75 	 */
     76 	if (payload_len < 132 /*|| plen > 87472*/) {
     77 		return NIP44_ERR_INVALID_PAYLOAD;
     78 	}
     79 
     80 	/*
     81 	1. Check if first payload's character is `#`
     82 
     83 	   - `#` is an optional future-proof flag that means non-base64
     84 	     encoding is used
     85 
     86 	   - The `#` is not present in base64 alphabet, but, instead of
     87 	     throwing `base64 is invalid`, implementations MUST indicate that
     88 	     the encryption version is not yet supported
     89 	*/ 
     90 	if (payload[0] == '#') {
     91 		return NIP44_ERR_UNSUPPORTED_ENCODING;
     92 	}
     93 
     94 	/*
     95 	2. Decode base64
     96 	   - Base64 is decoded into `version, nonce, ciphertext, mac`
     97 
     98 	   - If the version is unknown, implementations must indicate that the
     99 	     encryption version is not supported
    100 
    101 	   - Validate length of base64 message to prevent DoS on base64
    102 	     decoder: it can be in range from 132 to 87472 chars
    103 
    104 	   - Validate length of decoded message to verify output of the
    105 	     decoder: it can be in range from 99 to 65603 bytes
    106 	*/
    107 	decoded_len = base64_decode((char*)buf, bufsize, payload, payload_len);
    108 	if (decoded_len == -1) {
    109 		return NIP44_ERR_BASE64_DECODE;
    110 	} else if (decoded_len < 99 /*|| decoded_len > 65603*/) {
    111 		return NIP44_ERR_INVALID_PAYLOAD;
    112 	}
    113 
    114 	decoded->version = buf[0];
    115 	decoded->nonce = &buf[1];
    116 	decoded->ciphertext = &buf[33];
    117 	decoded->ciphertext_len = decoded_len - 65;
    118 	decoded->mac = &buf[decoded_len-32];
    119 
    120 	return NIP44_OK;
    121 }
    122 
    123 static inline uint16_t next_pow2_16(uint16_t v)
    124 {
    125 	if (v <= 1)
    126 		return 1;
    127 
    128 	v--; /* round down from v to (v-1) */
    129 	v |= v >> 1;
    130 	v |= v >> 2;
    131 	v |= v >> 4;
    132 	v |= v >> 8;
    133 	v++; /* now v is next power of two */
    134 
    135 	return v;
    136 }
    137 
    138 static int calc_padded_len(uint16_t unpadded_len)
    139 {
    140 	uint16_t chunk;
    141 
    142 	/* enforce minimum of 32 */
    143 	if (unpadded_len <= 32)
    144 		return 32;
    145 
    146 	/* For <= 256, always use 32-byte chunks. */
    147 	if (unpadded_len <= 256) {
    148 		chunk = 32;
    149 	} else {
    150 		/* next_power / 8 */
    151 		chunk = next_pow2_16(unpadded_len) >> 3;
    152 	}
    153 
    154 	chunk--;
    155 
    156 	// Round up to the next multiple of chunk (chunk is power of two)
    157 	return (unpadded_len + chunk) & ~chunk;
    158 }
    159 
    160 static int unpad(unsigned char *padded_buf, size_t len, uint16_t *unpadded_len)
    161 {
    162 	struct cursor c;
    163 	unsigned char *decoded_end;
    164 	uint16_t decoded_len, decoded_end_len;
    165 
    166 	make_cursor(padded_buf, padded_buf+len, &c);
    167 
    168 	if (!cursor_pull_b16(&c, &decoded_len)) {
    169 		fprintf(stderr, "unpad: couldn't pull decoded len\n");
    170 		return 0;
    171 	}
    172 
    173 	decoded_end_len = decoded_len + 2;
    174 	decoded_end = padded_buf + decoded_end_len;
    175 
    176 	if (decoded_end > c.end) {
    177 		fprintf(stderr, "decode debug: '%.*s'\n", (int)len-2, (const char *)(padded_buf + 2));
    178 		fprintf(stderr, "unpad: decoded end (%d) is larger then original buf (%ld)\n",
    179 				decoded_end_len, len);
    180 		return 0;
    181 	}
    182 
    183 	c.end = decoded_end;
    184 
    185 	*unpadded_len = (uint16_t)cursor_remaining_capacity(&c);
    186 
    187 	if (*unpadded_len != decoded_len) {
    188 		fprintf(stderr, "unpadded_len(%d) != decoded_len(%d)\n",
    189 			*unpadded_len, decoded_len);
    190 		return 0;
    191 	}
    192 
    193 	if (decoded_len == 0 || len != (2 + calc_padded_len(decoded_len))) {
    194 		fprintf(stderr, "padding size is wrong\n");
    195 		return 0;
    196 	}
    197 
    198 	return 1;
    199 }
    200 
    201 const char *nip44_err_msg(enum ndb_decrypt_result res)
    202 {
    203 	switch (res) {
    204 	case NIP44_OK:
    205 		return "ok";
    206 	case NIP44_ERR_FILL_RANDOM_FAILED:
    207 	       return "fill random failed";
    208 	case NIP44_ERR_INVALID_MAC:
    209 	       return "invalid mac";
    210 	case NIP44_ERR_SECKEY_VERIFY_FAILED:
    211 	       return "seckey verify failed";
    212 	case NIP44_ERR_PUBKEY_PARSE_FAILED:
    213 	       return "pubkey parse failed";
    214 	case NIP44_ERR_ECDH_FAILED:
    215 	       return "ecdh failed";
    216 	case NIP44_ERR_INVALID_PAYLOAD:
    217 	       return "invalid payload";
    218 	case NIP44_ERR_UNSUPPORTED_ENCODING:
    219 	       return "unsupported encoding";
    220 	case NIP44_ERR_BASE64_DECODE:
    221 	       return "error during base64 decoding";
    222 	case NIP44_ERR_INVALID_PADDING:
    223 	       return "invalid padding";
    224 	case NIP44_ERR_BUFFER_TOO_SMALL:
    225 	       return "buffer too small";
    226 	}
    227 
    228 	return "unknown";
    229 }
    230 
    231 /* ### Decryption
    232  * Before decryption, the event's pubkey and signature MUST be validated as
    233  * defined in NIP 01. The public key MUST be a valid non-zero secp256k1 curve
    234  * point, and the signature must be valid secp256k1 schnorr signature. For exact
    235  * validation rules, refer to BIP-340.
    236  */
    237 enum ndb_decrypt_result
    238 nip44_decrypt_raw(void *secp,
    239 	      const unsigned char *sender_pubkey,
    240 	      const unsigned char *receiver_seckey,
    241 	      struct nip44_payload *decoded,
    242 	      unsigned char **decrypted, uint16_t *decrypted_len)
    243 {
    244 	struct hmac_sha256 conversation_key;
    245 	struct hmac_sha256 calculated_mac;
    246 	enum ndb_decrypt_result rc;
    247 	unsigned char shared_secret[32];
    248 	struct message_keys keys;
    249 	secp256k1_context *context = (secp256k1_context *)secp;
    250 
    251 	/*
    252 	3. Calculate a conversation key
    253 	   - Execute ECDH (scalar multiplication) of public key B by private
    254 	     key A Output `shared_x` must be unhashed, 32-byte encoded x
    255 	     coordinate of the shared point
    256 	   - Use HKDF-extract with sha256, `IKM=shared_x` and
    257 	     `salt=utf8_encode('nip44-v2')`
    258 	   - HKDF output will be a `conversation_key` between two users.
    259 	*/
    260 	if ((rc = calculate_shared_secret(context, receiver_seckey,
    261 					  sender_pubkey, shared_secret))) {
    262 		return rc;
    263 	}
    264 
    265 	hmac_sha256(&conversation_key, "nip44-v2", 8, shared_secret, 32);
    266 
    267 	/*
    268 	5. Calculate message keys
    269 	   - The keys are generated from `conversation_key` and `nonce`.
    270 	     Validate that both are 32 bytes long
    271 	   - Use HKDF-expand, with sha256, `PRK=conversation_key`,
    272 	                                   `info=nonce` and `L=76`
    273 	   - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32),
    274 	     `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76)
    275 	*/
    276 	assert(sizeof(keys) == 76);
    277 	assert(sizeof(conversation_key) == 32);
    278 
    279 	hkdf_expand(&keys, sizeof(keys),
    280 		    &conversation_key, sizeof(conversation_key),
    281 		    decoded->nonce, 32);
    282 
    283 	/*
    284 	6. Calculate MAC (message authentication code) with AAD and compare
    285 	   - Stop and throw an error if MAC doesn't match the decoded one from
    286 	     step 2
    287 	   - Use constant-time comparison algorithm
    288 	*/
    289 	hmac_aad(&calculated_mac, keys.auth, decoded->nonce,
    290 		 decoded->ciphertext, decoded->ciphertext_len);
    291 
    292 	/* TODO(jb55): spec says this needs to be constant time memcmp,
    293 	 *             not sure why?
    294 	 */
    295 	if (memcmp(calculated_mac.sha.u.u8, decoded->mac, 32)) {
    296 		return NIP44_ERR_INVALID_MAC;
    297 	}
    298 
    299 
    300 	/*
    301 	6. Decrypt ciphertext
    302 	   - Use ChaCha20 with key and nonce from step 3
    303 	*/
    304 	crypto_stream_chacha20_ietf_xor_ic(decoded->ciphertext,
    305 					   decoded->ciphertext,
    306 					   decoded->ciphertext_len,
    307 					   keys.nonce, 0, keys.key);
    308 
    309 	/*
    310 	7. Remove padding
    311 	*/
    312 	if (!unpad(decoded->ciphertext, decoded->ciphertext_len, decrypted_len)) {
    313 		return NIP44_ERR_INVALID_PADDING;
    314 	}
    315 
    316 	*decrypted = decoded->ciphertext + 2;
    317 
    318 	return NIP44_OK;
    319 }
    320 
    321 enum ndb_decrypt_result
    322 nip44_decrypt(void *secp,
    323 	      const unsigned char *sender_pubkey,
    324 	      const unsigned char *receiver_seckey,
    325 	      const char *payload, int payload_len,
    326 	      unsigned char *buf, size_t bufsize,
    327 	      unsigned char **decrypted, uint16_t *decrypted_len)
    328 {
    329 	struct nip44_payload decoded;
    330 	enum ndb_decrypt_result rc;
    331 
    332 	/* decode payload! */
    333 	if ((rc = nip44_decode_payload(&decoded, buf, bufsize,
    334 				       payload, payload_len))) {
    335 		return rc;
    336 	}
    337 
    338 	return nip44_decrypt_raw(secp, sender_pubkey, receiver_seckey,
    339 				 &decoded, decrypted, decrypted_len);
    340 }
    341 
    342 /* Encryption */
    343 enum ndb_decrypt_result
    344 nip44_encrypt(void *secp, const unsigned char *sender_seckey,
    345 	      const unsigned char *receiver_pubkey,
    346 	      const unsigned char *plaintext, uint16_t plaintext_size,
    347 	      unsigned char *buf, size_t bufsize,
    348 	      char **out, ssize_t *out_len)
    349 {
    350 	int rc;
    351 	struct cursor cursor;
    352 	struct hmac_sha256 auth, conversation_key;
    353 	unsigned char shared_secret[32];
    354 	unsigned char nonce[32];
    355 	unsigned char *ciphertext;
    356 	struct message_keys keys;
    357 	uint16_t ciphertext_len;
    358 	
    359 	make_cursor(buf, buf+bufsize, &cursor);
    360 
    361 	/* 
    362 	1. Calculate a conversation key
    363 	   - Execute ECDH (scalar multiplication) of public key B by private
    364 	     key A Output `shared_x` must be unhashed, 32-byte encoded x
    365 	     coordinate of the shared point
    366 
    367 	   - Use HKDF-extract with sha256, `IKM=shared_x` and
    368 	     `salt=utf8_encode('nip44-v2')`
    369 
    370 	   - HKDF output will be a `conversation_key` between two users.
    371 
    372 	   - It is always the same, when key roles are swapped:
    373 	     `conv(a, B) == conv(b, A)`
    374 	*/
    375 	if ((rc = calculate_shared_secret(secp, sender_seckey,
    376 					  receiver_pubkey, shared_secret))) {
    377 		return rc;
    378 	}
    379 
    380 	hmac_sha256(&conversation_key, "nip44-v2", 8, shared_secret, 32);
    381 	/*
    382 	2. Generate a random 32-byte nonce
    383 	   - Always use CSPRNG
    384 	   - Don't generate a nonce from message content
    385 	   - Don't re-use the same nonce between messages: doing so would make
    386 	     them decryptable, but won't leak the long-term key
    387 	*/
    388 	if (!fill_random(nonce, sizeof(nonce))) {
    389 		return NIP44_ERR_FILL_RANDOM_FAILED;
    390 	}
    391 
    392 	/*
    393 	3. Calculate message keys
    394 	   - The keys are generated from `conversation_key` and `nonce`.
    395 	     Validate that both are 32 bytes long
    396 	   - Use HKDF-expand, with sha256, `PRK=conversation_key`, `info=nonce`
    397 	     and `L=76`
    398 	   - Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32),
    399 	     `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76)
    400 	*/
    401 	hkdf_expand(&keys, sizeof(keys),
    402 		    &conversation_key, sizeof(conversation_key),
    403 		    nonce, 32);
    404 
    405 	/*
    406 	4. Add padding
    407 	   - Content must be encoded from UTF-8 into byte array
    408 	   - Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes
    409 	   - Padding format is: `[plaintext_length:u16][plaintext][zero_bytes]`
    410 	   - Padding algorithm is related to powers-of-two, with min padded msg
    411 	     size of 32 bytes
    412 	   - Plaintext length is encoded in big-endian as first 2 bytes of the
    413 	     padded blob
    414 	*/
    415 	if (!cursor_push_byte(&cursor, 0x02))
    416 		return NIP44_ERR_BUFFER_TOO_SMALL;
    417 	if (!cursor_push(&cursor, nonce, 32))
    418 		return NIP44_ERR_BUFFER_TOO_SMALL;
    419 
    420 	ciphertext = cursor.p;
    421 
    422 	if (!cursor_push_b16(&cursor, plaintext_size))
    423 		return NIP44_ERR_BUFFER_TOO_SMALL;
    424 	if (!cursor_push(&cursor, (unsigned char*)plaintext, plaintext_size))
    425 		return NIP44_ERR_BUFFER_TOO_SMALL;
    426 	if (!cursor_memset(&cursor, 0, calc_padded_len(plaintext_size) - plaintext_size))
    427 		return NIP44_ERR_BUFFER_TOO_SMALL;
    428 
    429 	ciphertext_len = cursor.p - ciphertext;
    430 	assert(ciphertext_len >= 132);
    431 	/*
    432 	5. Encrypt padded content
    433 	   - Use ChaCha20, with key and nonce from step 3
    434 	*/
    435 	crypto_stream_chacha20_ietf_xor_ic(ciphertext, ciphertext,
    436 					   ciphertext_len, keys.nonce, 0,
    437 					   keys.key);
    438 
    439 	/*
    440 	6. Calculate MAC (message authentication code)
    441 	   - AAD (additional authenticated data) is used - instead of
    442 	     calculating MAC on ciphertext, it's calculated over a concatenation
    443 	     of `nonce` and `ciphertext`
    444 
    445 	   - Validate that AAD (nonce) is 32 bytes
    446 	*/
    447 	hmac_aad(&auth, keys.auth, nonce, ciphertext, ciphertext_len);
    448 
    449 	if (!cursor_push(&cursor, auth.sha.u.u8, 32))
    450 		return NIP44_ERR_BUFFER_TOO_SMALL;
    451 
    452 	/*
    453 	7. Base64-encode (with padding) params using `concat(version, nonce,
    454 	ciphertext, mac)`
    455 	*/
    456 	*out = (char*)cursor.p;
    457 	*out_len = base64_encode((char*)cursor.p,
    458 				 cursor_remaining_capacity(&cursor),
    459 				 (const char*)cursor.start,
    460 				 cursor.p - cursor.start);
    461 
    462 	if (*out_len == -1)
    463 		return NIP44_ERR_BUFFER_TOO_SMALL;
    464 	return NIP44_OK;
    465 }
    466