nostril

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

commit 6aa05ec5c34ec53a7e882ddbed2f50ab0857ee15
parent e754b81511ebe2eaf996282e7c3d40644317caa6
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 27 Nov 2025 07:21:43 -0800

initial giftwrap support

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
M.gitignore | 1+
M.gitmodules | 3+++
MMakefile | 22++++++++++++++++------
Mcursor.h | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adeps/libsodium | 1+
Mhex.h | 4++++
Ahkdf_sha256.c | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahkdf_sha256.h | 28++++++++++++++++++++++++++++
Ahmac_sha256.c | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahmac_sha256.h | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anip44.c | 466+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anip44.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnostril.c | 241++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mshell.nix | 1+
14 files changed, 1195 insertions(+), 46 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -3,6 +3,7 @@ .privenv nostril /result +build.log configurator.out* configurator .build-result diff --git a/.gitmodules b/.gitmodules @@ -1,3 +1,6 @@ [submodule "deps/secp256k1"] path = deps/secp256k1 url = https://github.com/bitcoin-core/secp256k1 +[submodule "deps/libsodium"] + path = deps/libsodium + url = https://github.com/jedisct1/libsodium.git diff --git a/Makefile b/Makefile @@ -1,11 +1,13 @@ -CFLAGS = -Wall -O2 -Ideps/secp256k1/include -OBJS = sha256.o nostril.o aes.o base64.o -HEADERS = hex.h random.h config.h sha256.h deps/secp256k1/include/secp256k1.h +CFLAGS = -Wall -O2 -Ideps/secp256k1/include -Ideps/libsodium/src/libsodium/include/ +LDFLAGS = -lm +OBJS = sha256.o nostril.o aes.o base64.o nip44.o hmac_sha256.o hkdf_sha256.o +HEADERS = hex.h nip44.h cursor.h random.h config.h sha256.h deps/secp256k1/include/secp256k1.h PREFIX ?= /usr/local -ARS = libsecp256k1.a +LIBSODIUM_AR=deps/libsodium/src/libsodium/.libs/libsodium.a +ARS = libsecp256k1.a $(LIBSODIUM_AR) -SUBMODULES = deps/secp256k1 +SUBMODULES = deps/secp256k1 deps/libsodium all: nostril docs @@ -27,6 +29,14 @@ dist: docs version cp CHANGELOG dist/CHANGELOG.txt rsync -avzP dist/ charon:/www/cdn.jb55.com/tarballs/nostril/ +$(LIBSODIUM_AR): deps/libsodium/config.log + cd deps/libsodium/src/libsodium; \ + make -j libsodium.la + +deps/libsodium/config.log: deps/libsodium/configure + cd deps/libsodium; \ + ./configure --disable-shared --enable-minimal + deps/secp256k1/.git: @devtools/refresh-submodules.sh $(SUBMODULES) @@ -52,7 +62,7 @@ libsecp256k1.a: deps/secp256k1/.libs/libsecp256k1.a @$(CC) $(CFLAGS) -c $< -o $@ nostril: $(HEADERS) $(OBJS) $(ARS) - $(CC) $(CFLAGS) $(OBJS) $(ARS) -o $@ + $(CC) $(CFLAGS) $(LDFLAGS) $(OBJS) $(ARS) -o $@ install: all mkdir -p $(PREFIX)/share/man/man1 diff --git a/cursor.h b/cursor.h @@ -5,6 +5,7 @@ #include <stdio.h> #include <assert.h> #include <string.h> +#include "hex.h" #define unlikely(x) __builtin_expect((x),0) #define likely(x) __builtin_expect((x),1) @@ -254,11 +255,42 @@ static inline int cursor_pull_int(struct cursor *cursor, int *i) return cursor_pull(cursor, (unsigned char*)i, sizeof(*i)); } +static inline int cursor_pull_u16(struct cursor *cursor, uint16_t *i) +{ + return cursor_pull(cursor, (unsigned char*)i, sizeof(*i)); +} + +#define BSWAP_16(val) \ + ((((uint16_t)(val) & 0x00ff) << 8) \ + | (((uint16_t)(val) & 0xff00) >> 8)) + +static inline uint16_t bswap_16(uint16_t val) +{ + return BSWAP_16(val); +} + static inline int cursor_push_u16(struct cursor *cursor, unsigned short i) { return cursor_push(cursor, (unsigned char*)&i, sizeof(i)); } +static int cursor_pull_b16(struct cursor *c, uint16_t *s) +{ + if (!cursor_pull_u16(c, s)) + return 0; + + // we assume little endian + *s = bswap_16(*s); + return 1; +} + +static int cursor_push_b16(struct cursor *c, uint16_t s) +{ + if (!cursor_push_u16(c, bswap_16(s))) + return 0; + return 1; +} + static inline void *index_cursor(struct cursor *cursor, unsigned int index, int elem_size) { unsigned char *p; @@ -291,6 +323,16 @@ static inline int cursor_remaining_capacity(struct cursor *cursor) return cursor->end - cursor->p; } +static inline int cursor_push_hex(struct cursor *c, const void *buf, size_t bufsize) +{ + int size; + size = hex_encode(buf, bufsize, (char *)c->p, c->end - c->p); + if (!size) + return 0; + c->p += bufsize * 2; + return 1; +} + #define max(a,b) ((a) > (b) ? (a) : (b)) static inline void cursor_print_around(struct cursor *cur, int range) @@ -317,4 +359,28 @@ static inline void cursor_print_around(struct cursor *cur, int range) } #undef max +static inline int cursor_memset(struct cursor *cursor, unsigned char c, int n) +{ + if (cursor->p + n >= cursor->end) + return 0; + + memset(cursor->p, c, n); + cursor->p += n; + + return 1; +} + + +static inline int cursor_align(struct cursor *cur, int bytes) { + size_t size = cur->p - cur->start; + int pad; + + // pad to n-byte alignment + pad = ((size + (bytes-1)) & ~(bytes-1)) - size; + if (pad > 0 && !cursor_memset(cur, 0, pad)) + return 0; + + return 1; +} + #endif diff --git a/deps/libsodium b/deps/libsodium @@ -0,0 +1 @@ +Subproject commit 9511c982fb1d046470a8b42aa36556cdb7da15de diff --git a/hex.h b/hex.h @@ -1,4 +1,7 @@ +#ifndef NOSTRIL_HEX_H +#define NOSTRIL_HEX_H + static inline int char_to_hex(unsigned char *val, char c) { if (c >= '0' && c <= '9') { @@ -67,3 +70,4 @@ static inline int hex_encode(const void *buf, size_t bufsize, char *dest, size_t return 1; } +#endif /* NOSTRIL_HEX_H */ diff --git a/hkdf_sha256.c b/hkdf_sha256.c @@ -0,0 +1,104 @@ +/* MIT (BSD) license - see LICENSE file for details */ +#include "hkdf_sha256.h" +#include "hmac_sha256.h" +#include <assert.h> +#include <string.h> + +void hkdf_expand(void *okm, size_t okm_size, + const void *prk, size_t prksize, + const void *info, size_t isize) +{ + struct hmac_sha256_ctx ctx; + struct hmac_sha256 t; + unsigned char c; + + assert(okm_size < 255 * sizeof(t)); + /* + * 2.3. Step 2: Expand + * + * HKDF-Expand(PRK, info, L) -> OKM + * + * Options: + * Hash a hash function; HashLen denotes the length of the + * hash function output in octets + * + * Inputs: + * PRK a pseudorandom key of at least HashLen octets + * (usually, the output from the extract step) + * info optional context and application specific information + * (can be a zero-length string) + * L length of output keying material in octets + * (<= 255*HashLen) + * + * Output: + * OKM output keying material (of L octets) + * + * The output OKM is calculated as follows: + * + * N = ceil(L/HashLen) + * T = T(1) | T(2) | T(3) | ... | T(N) + * OKM = first L octets of T + * + * where: + * T(0) = empty string (zero length) + * T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) + * T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) + * T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) + * ... + * + * (where the constant concatenated to the end of each T(n) is a + * single octet.) + */ + c = 1; + hmac_sha256_init(&ctx, prk, prksize); + hmac_sha256_update(&ctx, info, isize); + hmac_sha256_update(&ctx, &c, 1); + hmac_sha256_done(&ctx, &t); + + while (okm_size > sizeof(t)) { + memcpy(okm, &t, sizeof(t)); + okm = (char *)okm + sizeof(t); + okm_size -= sizeof(t); + + c++; + hmac_sha256_init(&ctx, prk, prksize); + hmac_sha256_update(&ctx, &t, sizeof(t)); + hmac_sha256_update(&ctx, info, isize); + hmac_sha256_update(&ctx, &c, 1); + hmac_sha256_done(&ctx, &t); + } + memcpy(okm, &t, okm_size); +} + +void hkdf_sha256(void *okm, size_t okm_size, + const void *s, size_t ssize, + const void *k, size_t ksize, + const void *info, size_t isize) +{ + struct hmac_sha256 prk; + + /* RFC 5869: + * + * 2.2. Step 1: Extract + * + * HKDF-Extract(salt, IKM) -> PRK + * + * Options: + * Hash a hash function; HashLen denotes the length of the + * hash function output in octets + * + * Inputs: + * salt optional salt value (a non-secret random value); + * if not provided, it is set to a string of HashLen zeros. + * IKM input keying material + * + * Output: + * PRK a pseudorandom key (of HashLen octets) + * + * The output PRK is calculated as follows: + * + * PRK = HMAC-Hash(salt, IKM) + */ + hmac_sha256(&prk, s, ssize, k, ksize); + hkdf_expand(okm, okm_size, &prk, sizeof(prk), info, isize); +} diff --git a/hkdf_sha256.h b/hkdf_sha256.h @@ -0,0 +1,28 @@ +#ifndef CCAN_CRYPTO_HKDF_SHA256_H +#define CCAN_CRYPTO_HKDF_SHA256_H +/* BSD-MIT - see LICENSE file for details */ +#include "config.h" +#include "hmac_sha256.h" +#include <stdlib.h> + +/** + * hkdf_sha256 - generate a derived key + * @okm: where to output the key + * @okm_size: the number of bytes pointed to by @okm (must be less than 255*32) + * @s: salt + * @ssize: the number of bytes pointed to by @s + * @k: pointer to input key + * @ksize: the number of bytes pointed to by @k + * @info: pointer to info + * @isize: the number of bytes pointed to by @info + */ +void hkdf_sha256(void *okm, size_t okm_size, + const void *s, size_t ssize, + const void *k, size_t ksize, + const void *info, size_t isize); + +void hkdf_expand(void *okm, size_t okm_size, + const void *prk, size_t prksize, + const void *info, size_t isize); + +#endif /* CCAN_CRYPTO_HKDF_SHA256_H */ diff --git a/hmac_sha256.c b/hmac_sha256.c @@ -0,0 +1,161 @@ +/* MIT (BSD) license - see LICENSE file for details */ +#include "hmac_sha256.h" +#include <string.h> + +#define IPAD 0x3636363636363636ULL +#define OPAD 0x5C5C5C5C5C5C5C5CULL + +#define BLOCK_U64S (HMAC_SHA256_BLOCKSIZE / sizeof(uint64_t)) + +static inline void xor_block(uint64_t block[BLOCK_U64S], uint64_t pad) +{ + size_t i; + + for (i = 0; i < BLOCK_U64S; i++) + block[i] ^= pad; +} + +void hmac_sha256_init(struct hmac_sha256_ctx *ctx, + const void *k, size_t ksize) +{ + struct sha256 hashed_key; + /* We use k_opad as k_ipad temporarily. */ + uint64_t *k_ipad = ctx->k_opad; + + /* (keys longer than B bytes are first hashed using H) */ + if (ksize > HMAC_SHA256_BLOCKSIZE) { + sha256(&hashed_key, k, ksize); + k = &hashed_key; + ksize = sizeof(hashed_key); + } + + /* From RFC2104: + * + * (1) append zeros to the end of K to create a B byte string + * (e.g., if K is of length 20 bytes and B=64, then K will be + * appended with 44 zero bytes 0x00) + */ + if (ksize != 0) + memcpy(k_ipad, k, ksize); + memset((char *)k_ipad + ksize, 0, HMAC_SHA256_BLOCKSIZE - ksize); + + /* + * (2) XOR (bitwise exclusive-OR) the B byte string computed + * in step (1) with ipad + */ + xor_block(k_ipad, IPAD); + + /* + * We start (4) here, appending text later: + * + * (3) append the stream of data 'text' to the B byte string resulting + * from step (2) + * (4) apply H to the stream generated in step (3) + */ + sha256_init(&ctx->sha); + sha256_update(&ctx->sha, k_ipad, HMAC_SHA256_BLOCKSIZE); + + /* + * (5) XOR (bitwise exclusive-OR) the B byte string computed in + * step (1) with opad + */ + xor_block(ctx->k_opad, IPAD^OPAD); +} + +void hmac_sha256_update(struct hmac_sha256_ctx *ctx, const void *p, size_t size) +{ + /* This is the appending-text part of this: + * + * (3) append the stream of data 'text' to the B byte string resulting + * from step (2) + * (4) apply H to the stream generated in step (3) + */ + sha256_update(&ctx->sha, p, size); +} + +void hmac_sha256_done(struct hmac_sha256_ctx *ctx, + struct hmac_sha256 *hmac) +{ + /* (4) apply H to the stream generated in step (3) */ + sha256_done(&ctx->sha, &hmac->sha); + + /* + * (6) append the H result from step (4) to the B byte string + * resulting from step (5) + * (7) apply H to the stream generated in step (6) and output + * the result + */ + sha256_init(&ctx->sha); + sha256_update(&ctx->sha, ctx->k_opad, sizeof(ctx->k_opad)); + sha256_update(&ctx->sha, &hmac->sha, sizeof(hmac->sha)); + sha256_done(&ctx->sha, &hmac->sha); +} + +#if 1 +void hmac_sha256(struct hmac_sha256 *hmac, + const void *k, size_t ksize, + const void *d, size_t dsize) +{ + struct hmac_sha256_ctx ctx; + + hmac_sha256_init(&ctx, k, ksize); + hmac_sha256_update(&ctx, d, dsize); + hmac_sha256_done(&ctx, hmac); +} +#else +/* Direct mapping from MD5 example in RFC2104 */ +void hmac_sha256(struct hmac_sha256 *hmac, + const void *key, size_t key_len, + const void *text, size_t text_len) +{ + struct sha256_ctx context; + unsigned char k_ipad[65]; /* inner padding - + * key XORd with ipad + */ + unsigned char k_opad[65]; /* outer padding - + * key XORd with opad + *//* start out by storing key in pads */ + unsigned char tk[32]; + int i; + + /* if key is longer than 64 bytes reset it to key=MD5(key) */ + if (key_len > 64) { + + struct sha256_ctx tctx; + + sha256_init(&tctx); + sha256_update(&tctx, key, key_len); + sha256_done(&tctx, tk); + + key = tk; + key_len = 32; + } + bzero( k_ipad, sizeof k_ipad); + bzero( k_opad, sizeof k_opad); + bcopy( key, k_ipad, key_len); + bcopy( key, k_opad, key_len); + + /* XOR key with ipad and opad values */ + for (i=0; i<64; i++) { + k_ipad[i] ^= 0x36; + k_opad[i] ^= 0x5c; + } + /* + * perform inner MD5 + */ + sha256_init(&context); /* init context for 1st + * pass */ + sha256_update(&context, k_ipad, 64); /* start with inner pad */ + sha256_update(&context, text, text_len); /* then text of datagram */ + sha256_done(&context, &hmac->sha); /* finish up 1st pass */ + /* + * perform outer MD5 + */ + sha256_init(&context); /* init context for 2nd + * pass */ + sha256_update(&context, k_opad, 64); /* start with outer pad */ + sha256_update(&context, &hmac->sha, 32); /* then results of 1st + * hash */ + sha256_done(&context, &hmac->sha); /* finish up 2nd pass */ +} +#endif diff --git a/hmac_sha256.h b/hmac_sha256.h @@ -0,0 +1,86 @@ +#ifndef CCAN_CRYPTO_HMAC_SHA256_H +#define CCAN_CRYPTO_HMAC_SHA256_H +/* BSD-MIT - see LICENSE file for details */ +#include "config.h" +#include <stdint.h> +#include <stdlib.h> +#include "sha256.h" + +/* Number of bytes per block. */ +#define HMAC_SHA256_BLOCKSIZE 64 + +/** + * struct hmac_sha256 - structure representing a completed HMAC. + */ +struct hmac_sha256 { + struct sha256 sha; +}; + +/** + * hmac_sha256 - return hmac of an object with a key. + * @hmac: the hmac to fill in + * @k: pointer to the key, + * @ksize: the number of bytes pointed to by @k + * @d: pointer to memory, + * @dsize: the number of bytes pointed to by @d + */ +void hmac_sha256(struct hmac_sha256 *hmac, + const void *k, size_t ksize, + const void *d, size_t dsize); + +/** + * struct hmac_sha256_ctx - structure to store running context for hmac_sha256 + */ +struct hmac_sha256_ctx { + struct sha256_ctx sha; + uint64_t k_opad[HMAC_SHA256_BLOCKSIZE / sizeof(uint64_t)]; +}; + +/** + * hmac_sha256_init - initialize an HMAC_SHA256 context. + * @ctx: the hmac_sha256_ctx to initialize + * @k: pointer to the key, + * @ksize: the number of bytes pointed to by @k + * + * This must be called before hmac_sha256_update or hmac_sha256_done. + * + * If it was already initialized, this forgets anything which was + * hashed before. + * + * Example: + * static void hmac_all(const char *key, + * const char **arr, struct hmac_sha256 *hash) + * { + * size_t i; + * struct hmac_sha256_ctx ctx; + * + * hmac_sha256_init(&ctx, key, strlen(key)); + * for (i = 0; arr[i]; i++) + * hmac_sha256_update(&ctx, arr[i], strlen(arr[i])); + * hmac_sha256_done(&ctx, hash); + * } + */ +void hmac_sha256_init(struct hmac_sha256_ctx *ctx, + const void *k, size_t ksize); + +/** + * hmac_sha256_update - include some memory in the hash. + * @ctx: the hmac_sha256_ctx to use + * @p: pointer to memory, + * @size: the number of bytes pointed to by @p + * + * You can call this multiple times to hash more data, before calling + * hmac_sha256_done(). + */ +void hmac_sha256_update(struct hmac_sha256_ctx *ctx, const void *p, size_t size); + +/** + * hmac_sha256_done - finish HMAC_SHA256 and return the hash + * @ctx: the hmac_sha256_ctx to complete + * @res: the hash to return. + * + * Note that @ctx is *destroyed* by this, and must be reinitialized. + * To avoid that, pass a copy instead. + */ +void hmac_sha256_done(struct hmac_sha256_ctx *hmac_sha256, struct hmac_sha256 *res); +#endif /* CCAN_CRYPTO_HMAC_SHA256_H */ diff --git a/nip44.c b/nip44.c @@ -0,0 +1,466 @@ + +#include "base64.h" +#include "secp256k1.h" +#include "secp256k1_ecdh.h" +#include "secp256k1_schnorrsig.h" +#include "hmac_sha256.h" +#include "hkdf_sha256.h" +#include "nip44.h" +#include "random.h" +#include "cursor.h" +#include "sodium/crypto_stream_chacha20.h" +#include <string.h> + +/* NIP44 payload encryption/decryption */ + +static int copyx(unsigned char *output, const unsigned char *x32, + const unsigned char *y32, void *data) +{ + memcpy(output, x32, 32); + return 1; +} + +static enum ndb_decrypt_result +calculate_shared_secret(secp256k1_context *ctx, + const unsigned char *seckey, + const unsigned char *pubkey, + unsigned char *shared_secret) +{ + secp256k1_pubkey parsed_pubkey; + unsigned char compressed_pubkey[33]; + compressed_pubkey[0] = 2; + memcpy(&compressed_pubkey[1], pubkey, 32); + + if (!secp256k1_ec_seckey_verify(ctx, seckey)) { + return NIP44_ERR_SECKEY_VERIFY_FAILED; + } + + if (!secp256k1_ec_pubkey_parse(ctx, &parsed_pubkey, compressed_pubkey, sizeof(compressed_pubkey))) { + return NIP44_ERR_PUBKEY_PARSE_FAILED; + } + + if (!secp256k1_ecdh(ctx, shared_secret, &parsed_pubkey, seckey, copyx, NULL)) { + return NIP44_ERR_ECDH_FAILED; + } + + return NIP44_OK; +} + +struct message_keys { + unsigned char key[32]; + unsigned char nonce[12]; + unsigned char auth[32]; +}; + +static void hmac_aad(struct hmac_sha256 *out, + unsigned char hmac[32], unsigned char *aad, + const unsigned char *msg, size_t msgsize) +{ + struct hmac_sha256_ctx ctx; + hmac_sha256_init(&ctx, hmac, 32); + hmac_sha256_update(&ctx, aad, 32); + hmac_sha256_update(&ctx, msg, msgsize); + hmac_sha256_done(&ctx, out); +} + +enum ndb_decrypt_result +nip44_decode_payload(struct nip44_payload *decoded, + unsigned char *buf, size_t bufsize, + const char *payload, size_t payload_len) +{ + size_t decoded_len; + + /* NOTE(jb55): we use the variant that doesn't have an + * upper size limit + */ + if (payload_len < 132 /*|| plen > 87472*/) { + return NIP44_ERR_INVALID_PAYLOAD; + } + + /* + 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`, implementations MUST indicate that + the encryption version is not yet supported + */ + if (payload[0] == '#') { + return NIP44_ERR_UNSUPPORTED_ENCODING; + } + + /* + 2. Decode base64 + - Base64 is decoded into `version, nonce, ciphertext, mac` + + - If the version is unknown, implementations must indicate that the + encryption version is not 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 + */ + decoded_len = base64_decode((char*)buf, bufsize, payload, payload_len); + if (decoded_len == -1) { + return NIP44_ERR_BASE64_DECODE; + } else if (decoded_len < 99 /*|| decoded_len > 65603*/) { + return NIP44_ERR_INVALID_PAYLOAD; + } + + decoded->version = buf[0]; + decoded->nonce = &buf[1]; + decoded->ciphertext = &buf[33]; + decoded->ciphertext_len = decoded_len - 65; + decoded->mac = &buf[decoded_len-32]; + + return NIP44_OK; +} + +static inline uint16_t next_pow2_16(uint16_t v) +{ + if (v <= 1) + return 1; + + v--; /* round down from v to (v-1) */ + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v++; /* now v is next power of two */ + + return v; +} + +static int calc_padded_len(uint16_t unpadded_len) +{ + uint16_t chunk; + + /* enforce minimum of 32 */ + if (unpadded_len <= 32) + return 32; + + /* For <= 256, always use 32-byte chunks. */ + if (unpadded_len <= 256) { + chunk = 32; + } else { + /* next_power / 8 */ + chunk = next_pow2_16(unpadded_len) >> 3; + } + + chunk--; + + // Round up to the next multiple of chunk (chunk is power of two) + return (unpadded_len + chunk) & ~chunk; +} + +static int unpad(unsigned char *padded_buf, size_t len, uint16_t *unpadded_len) +{ + struct cursor c; + unsigned char *decoded_end; + uint16_t decoded_len, decoded_end_len; + + make_cursor(padded_buf, padded_buf+len, &c); + + if (!cursor_pull_b16(&c, &decoded_len)) { + fprintf(stderr, "unpad: couldn't pull decoded len\n"); + return 0; + } + + decoded_end_len = decoded_len + 2; + decoded_end = padded_buf + decoded_end_len; + + if (decoded_end > c.end) { + fprintf(stderr, "decode debug: '%.*s'\n", (int)len-2, (const char *)(padded_buf + 2)); + fprintf(stderr, "unpad: decoded end (%d) is larger then original buf (%ld)\n", + decoded_end_len, len); + return 0; + } + + c.end = decoded_end; + + *unpadded_len = (uint16_t)cursor_remaining_capacity(&c); + + if (*unpadded_len != decoded_len) { + fprintf(stderr, "unpadded_len(%d) != decoded_len(%d)\n", + *unpadded_len, decoded_len); + return 0; + } + + if (decoded_len == 0 || len != (2 + calc_padded_len(decoded_len))) { + fprintf(stderr, "padding size is wrong\n"); + return 0; + } + + return 1; +} + +const char *nip44_err_msg(enum ndb_decrypt_result res) +{ + switch (res) { + case NIP44_OK: + return "ok"; + case NIP44_ERR_FILL_RANDOM_FAILED: + return "fill random failed"; + case NIP44_ERR_INVALID_MAC: + return "invalid mac"; + case NIP44_ERR_SECKEY_VERIFY_FAILED: + return "seckey verify failed"; + case NIP44_ERR_PUBKEY_PARSE_FAILED: + return "pubkey parse failed"; + case NIP44_ERR_ECDH_FAILED: + return "ecdh failed"; + case NIP44_ERR_INVALID_PAYLOAD: + return "invalid payload"; + case NIP44_ERR_UNSUPPORTED_ENCODING: + return "unsupported encoding"; + case NIP44_ERR_BASE64_DECODE: + return "error during base64 decoding"; + case NIP44_ERR_INVALID_PADDING: + return "invalid padding"; + case NIP44_ERR_BUFFER_TOO_SMALL: + return "buffer too small"; + } + + return "unknown"; +} + +/* ### Decryption + * Before decryption, the event's pubkey and signature MUST be validated as + * defined in NIP 01. The public key MUST be a valid non-zero secp256k1 curve + * point, and the signature must be valid secp256k1 schnorr signature. For exact + * validation rules, refer to BIP-340. + */ +enum ndb_decrypt_result +nip44_decrypt_raw(void *secp, + const unsigned char *sender_pubkey, + const unsigned char *receiver_seckey, + struct nip44_payload *decoded, + unsigned char **decrypted, uint16_t *decrypted_len) +{ + struct hmac_sha256 conversation_key; + struct hmac_sha256 calculated_mac; + enum ndb_decrypt_result rc; + unsigned char shared_secret[32]; + struct message_keys keys; + secp256k1_context *context = (secp256k1_context *)secp; + + /* + 3. Calculate a 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 a `conversation_key` between two users. + */ + if ((rc = calculate_shared_secret(context, receiver_seckey, + sender_pubkey, shared_secret))) { + return rc; + } + + hmac_sha256(&conversation_key, "nip44-v2", 8, shared_secret, 32); + + /* + 5. Calculate message keys + - The keys are generated from `conversation_key` and `nonce`. + Validate that both are 32 bytes long + - Use HKDF-expand, with sha256, `PRK=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) + */ + assert(sizeof(keys) == 76); + assert(sizeof(conversation_key) == 32); + + hkdf_expand(&keys, sizeof(keys), + &conversation_key, sizeof(conversation_key), + decoded->nonce, 32); + + /* + 6. 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 + */ + hmac_aad(&calculated_mac, keys.auth, decoded->nonce, + decoded->ciphertext, decoded->ciphertext_len); + + /* TODO(jb55): spec says this needs to be constant time memcmp, + * not sure why? + */ + if (memcmp(calculated_mac.sha.u.u8, decoded->mac, 32)) { + return NIP44_ERR_INVALID_MAC; + } + + + /* + 6. Decrypt ciphertext + - Use ChaCha20 with key and nonce from step 3 + */ + crypto_stream_chacha20_ietf_xor_ic(decoded->ciphertext, + decoded->ciphertext, + decoded->ciphertext_len, + keys.nonce, 0, keys.key); + + /* + 7. Remove padding + */ + if (!unpad(decoded->ciphertext, decoded->ciphertext_len, decrypted_len)) { + return NIP44_ERR_INVALID_PADDING; + } + + *decrypted = decoded->ciphertext + 2; + + return NIP44_OK; +} + +enum ndb_decrypt_result +nip44_decrypt(void *secp, + const unsigned char *sender_pubkey, + const unsigned char *receiver_seckey, + const char *payload, int payload_len, + unsigned char *buf, size_t bufsize, + unsigned char **decrypted, uint16_t *decrypted_len) +{ + struct nip44_payload decoded; + enum ndb_decrypt_result rc; + + /* decode payload! */ + if ((rc = nip44_decode_payload(&decoded, buf, bufsize, + payload, payload_len))) { + return rc; + } + + return nip44_decrypt_raw(secp, sender_pubkey, receiver_seckey, + &decoded, decrypted, decrypted_len); +} + +/* Encryption */ +enum ndb_decrypt_result +nip44_encrypt(void *secp, const unsigned char *sender_seckey, + const unsigned char *receiver_pubkey, + const unsigned char *plaintext, uint16_t plaintext_size, + unsigned char *buf, size_t bufsize, + char **out, ssize_t *out_len) +{ + int rc; + struct cursor cursor; + struct hmac_sha256 auth, conversation_key; + unsigned char shared_secret[32]; + unsigned char nonce[32]; + unsigned char *ciphertext; + struct message_keys keys; + uint16_t ciphertext_len; + + make_cursor(buf, buf+bufsize, &cursor); + + /* + 1. Calculate a 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 a `conversation_key` between two users. + + - It is always the same, when key roles are swapped: + `conv(a, B) == conv(b, A)` + */ + if ((rc = calculate_shared_secret(secp, sender_seckey, + receiver_pubkey, shared_secret))) { + return rc; + } + + hmac_sha256(&conversation_key, "nip44-v2", 8, shared_secret, 32); + /* + 2. Generate a random 32-byte nonce + - Always use CSPRNG + - Don't generate a nonce from message content + - Don't re-use the same nonce between messages: doing so would make + them decryptable, but won't leak the long-term key + */ + if (!fill_random(nonce, sizeof(nonce))) { + return NIP44_ERR_FILL_RANDOM_FAILED; + } + + /* + 3. Calculate message keys + - The keys are generated from `conversation_key` and `nonce`. + Validate that both are 32 bytes long + - Use HKDF-expand, with sha256, `PRK=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) + */ + hkdf_expand(&keys, sizeof(keys), + &conversation_key, sizeof(conversation_key), + nonce, 32); + + /* + 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 bytes + - Plaintext length is encoded in big-endian as first 2 bytes of the + padded blob + */ + if (!cursor_push_byte(&cursor, 0x02)) + return NIP44_ERR_BUFFER_TOO_SMALL; + if (!cursor_push(&cursor, nonce, 32)) + return NIP44_ERR_BUFFER_TOO_SMALL; + + ciphertext = cursor.p; + + if (!cursor_push_b16(&cursor, plaintext_size)) + return NIP44_ERR_BUFFER_TOO_SMALL; + if (!cursor_push(&cursor, (unsigned char*)plaintext, plaintext_size)) + return NIP44_ERR_BUFFER_TOO_SMALL; + if (!cursor_memset(&cursor, 0, calc_padded_len(plaintext_size) - plaintext_size)) + return NIP44_ERR_BUFFER_TOO_SMALL; + + ciphertext_len = cursor.p - ciphertext; + assert(ciphertext_len >= 132); + /* + 5. Encrypt padded content + - Use ChaCha20, with key and nonce from step 3 + */ + crypto_stream_chacha20_ietf_xor_ic(ciphertext, ciphertext, + ciphertext_len, keys.nonce, 0, + keys.key); + + /* + 6. Calculate MAC (message authentication code) + - AAD (additional authenticated data) 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 + */ + hmac_aad(&auth, keys.auth, nonce, ciphertext, ciphertext_len); + + if (!cursor_push(&cursor, auth.sha.u.u8, 32)) + return NIP44_ERR_BUFFER_TOO_SMALL; + + /* + 7. Base64-encode (with padding) params using `concat(version, nonce, + ciphertext, mac)` + */ + *out = (char*)cursor.p; + *out_len = base64_encode((char*)cursor.p, + cursor_remaining_capacity(&cursor), + (const char*)cursor.start, + cursor.p - cursor.start); + + if (*out_len == -1) + return NIP44_ERR_BUFFER_TOO_SMALL; + return NIP44_OK; +} + diff --git a/nip44.h b/nip44.h @@ -0,0 +1,57 @@ + +#ifndef NDB_NIP44_H +#define NDB_NIP44_H + +enum ndb_decrypt_result +{ + NIP44_OK = 0, + NIP44_ERR_UNSUPPORTED_ENCODING = 1, + NIP44_ERR_INVALID_PAYLOAD = 2, + NIP44_ERR_BASE64_DECODE = 3, + NIP44_ERR_SECKEY_VERIFY_FAILED = 4, + NIP44_ERR_PUBKEY_PARSE_FAILED = 5, + NIP44_ERR_ECDH_FAILED = 6, + NIP44_ERR_FILL_RANDOM_FAILED = 7, + NIP44_ERR_INVALID_MAC = 8, + NIP44_ERR_INVALID_PADDING = 9, + NIP44_ERR_BUFFER_TOO_SMALL = 10, +}; + +struct nip44_payload { + unsigned char version; + unsigned char *nonce; + unsigned char *ciphertext; + size_t ciphertext_len; + unsigned char *mac; +}; + +enum ndb_decrypt_result +nip44_decrypt(void *secp_context, + const unsigned char *sender_pubkey, + const unsigned char *receiver_seckey, + const char *payload, int payload_len, + unsigned char *buf, size_t bufsize, + unsigned char **decrypted, uint16_t *decrypted_len); + +enum ndb_decrypt_result +nip44_encrypt(void *secp, const unsigned char *sender_seckey, + const unsigned char *receiver_pubkey, + const unsigned char *plaintext, uint16_t plaintext_size, + unsigned char *buf, size_t bufsize, + char **out, ssize_t *out_len); + +enum ndb_decrypt_result +nip44_decrypt_raw(void *secp, + const unsigned char *sender_pubkey, + const unsigned char *receiver_seckey, + struct nip44_payload *decoded, + unsigned char **decrypted, uint16_t *decrypted_len); + +enum ndb_decrypt_result +nip44_decode_payload(struct nip44_payload *decoded, + unsigned char *buf, size_t bufsize, + const char *payload, size_t payload_len); + +const char *nip44_err_msg(enum ndb_decrypt_result res); + +#endif /* NDB_METADATA_H */ diff --git a/nostril.c b/nostril.c @@ -13,6 +13,7 @@ #include "cursor.h" #include "hex.h" +#include "nip44.h" #include "base64.h" #include "aes.h" #include "sha256.h" @@ -30,6 +31,7 @@ #define HAS_ENCRYPT (1<<4) #define HAS_DIFFICULTY (1<<5) #define HAS_MINE_PUBKEY (1<<6) +#define HAS_GIFTWRAP (1<<7) struct key { secp256k1_keypair pair; @@ -79,6 +81,7 @@ void usage() printf("\n"); printf(" --content <string> the content of the note\n"); printf(" --dm <hex pubkey> make an encrypted dm to said pubkey. sets kind and tags.\n"); + printf(" --giftwrap-to <hex pubkey> make an encrypted giftwrap to said pubkey.\n"); printf(" --envelope wrap in [\"EVENT\",...] for easy relaying\n"); printf(" --kind <number> set kind\n"); printf(" --created-at <unix timestamp> set a specific created-at time\n"); @@ -313,13 +316,12 @@ static int init_secp_context(secp256k1_context **ctx) return secp256k1_context_randomize(*ctx, randomize); } -static int generate_event_id(struct nostr_event *ev) +static int generate_event_id(struct nostr_event *ev, + unsigned char *buf, size_t bufsize) { - static unsigned char buf[102400]; - int len; - if (!(len = event_commitment(ev, buf, sizeof(buf)))) { + if (!(len = event_commitment(ev, buf, bufsize))) { fprintf(stderr, "event_commitment: buffer out of space\n"); return 0; } @@ -339,45 +341,65 @@ static int sign_event(secp256k1_context *ctx, struct key *key, struct nostr_even return 1; } -static int print_event(struct nostr_event *ev, int envelope) +static int all_zeros(unsigned char *buf, size_t bufsize) { - unsigned char buf[102400]; - char pubkey[65]; - char id[65]; - char sig[129]; - struct cursor cur; - int ok; + int i; - ok = hex_encode(ev->id, sizeof(ev->id), id, sizeof(id)) && - hex_encode(ev->pubkey, sizeof(ev->pubkey), pubkey, sizeof(pubkey)) && - hex_encode(ev->sig, sizeof(ev->sig), sig, sizeof(sig)); + for (i = 0; i < bufsize; i++) { + if (buf[i] != 0) + return 0; + } - assert(ok); + return 1; +} - make_cursor(buf, buf+sizeof(buf), &cur); - if (!cursor_push_tags(&cur, ev)) - return 0; +static int event_to_json(struct cursor *c, struct nostr_event *ev, int envelope) +{ + char shortbuf[128]; - if (envelope) - printf("[\"EVENT\","); + if (envelope) { + if (!cursor_push_str(c, "[\"EVENT\",")) + return 0; + } - printf("{\"id\": \"%s\",", id); - printf("\"pubkey\": \"%s\",", pubkey); - printf("\"created_at\": %" PRIu64 ",", ev->created_at); - printf("\"kind\": %d,", ev->kind); - printf("\"tags\": %.*s,", (int)cursor_len(&cur), cur.start); + if (!cursor_push_str(c, "{\"id\":\"")) return 0; + if (!cursor_push_hex(c, ev->id, 32)) return 0; + if (!cursor_push_str(c, "\",")) return 0; + if (!cursor_push_str(c, "\"pubkey\":\"")) return 0; + if (!cursor_push_hex(c, ev->pubkey, 32)) return 0; + if (!cursor_push_str(c, "\",")) return 0; + sprintf(shortbuf, "\"created_at\":%" PRIu64 ",", ev->created_at); + if (!cursor_push_str(c, shortbuf)) return 0; + sprintf(shortbuf, "\"kind\":%d,", ev->kind); + if (!cursor_push_str(c, shortbuf)) return 0; + if (!cursor_push_str(c, "\"tags\":")) return 0; + if (!cursor_push_tags(c, ev)) return 0; + if (!cursor_push_str(c, ",\"content\":")) return 0; + if (!cursor_push_jsonstr(c, ev->content)) return 0; + + if (!all_zeros(ev->sig, 64)) { + if (!cursor_push_str(c, ",\"sig\":\"")) return 0; + if (!cursor_push_hex(c, ev->sig, 64)) return 0; + if (!cursor_push_byte(c, '"')) return 0; + } - reset_cursor(&cur); - if (!cursor_push_jsonstr(&cur, ev->content)) - return 0; + if (envelope) { + if (!cursor_push_str(c, "]")) return 0; + } + + return cursor_push_c_str(c, "}"); +} - printf("\"content\": %.*s,", (int)cursor_len(&cur), cur.start); - printf("\"sig\": \"%s\"}", sig); +static int print_event(struct nostr_event *ev, int envelope, + unsigned char *buf, size_t bufsize) +{ + struct cursor cur; + make_cursor(buf, buf+bufsize, &cur); - if (envelope) - printf("]"); + if (!event_to_json(&cur, ev, envelope)) + return 0; - printf("\n"); + printf("%s\n", buf); return 1; } @@ -584,6 +606,13 @@ static int parse_args(int argc, const char *argv[], struct args *args, struct no } else if (!strcmp(arg, "--content")) { arg = *argv++; argc--; args->content = arg; + } else if (!strcmp(arg, "--giftwrap-to")) { + arg = *argv++; argc--; + if (!hex_decode(arg, strlen(arg), args->encrypt_to, 32)) { + fprintf(stderr, "could not decode giftwrap-to pubkey"); + return 0; + } + args->flags |= HAS_GIFTWRAP; } else { fprintf(stderr, "unexpected argument '%s'\n", arg); return 0; @@ -652,7 +681,8 @@ static int ensure_nonce_tag(struct nostr_event *ev, int target, int *index) return nostr_add_tag_n(ev, ts, 3); } -static int mine_event(struct nostr_event *ev, int difficulty) +static int mine_event(struct nostr_event *ev, int difficulty, + unsigned char *buf, size_t bufsize) { char *strnonce = malloc(33); struct nostr_tag *tag; @@ -670,7 +700,7 @@ static int mine_event(struct nostr_event *ev, int difficulty) for (nonce = 0;; nonce++) { snprintf(strnonce, 32, "%" PRIu64, nonce); - if (!generate_event_id(ev)) + if (!generate_event_id(ev, buf, bufsize)) return 0; if ((res = count_leading_zero_bits(ev->id)) >= difficulty) { @@ -682,6 +712,121 @@ static int mine_event(struct nostr_event *ev, int difficulty) return 0; } +static int make_giftwrap(secp256k1_context *ctx, struct key *key, + struct nostr_event *rumor, unsigned char receiver_pubkey[32], + unsigned char *buf, size_t bufsize, + int is_envelope) +{ + struct nostr_event giftwrap = {0}; + struct nostr_event seal = {0}; + struct key wrap_key; + char *b64, *json; + struct cursor arena, *c; + ssize_t b64_len; + enum ndb_decrypt_result ok; + + c = &arena; + make_cursor(buf, buf+bufsize, c); + if (!fill_random(wrap_key.secret, sizeof(wrap_key.secret))) + return 0; + create_key(ctx, &wrap_key); /* giftwrap key */ + + memcpy(rumor->pubkey, key->pubkey, 32); + if (!generate_event_id(rumor, c->p, cursor_remaining_capacity(c))) + return 0; + + /* rumor data */ + json = (char*)c->p; + if (!event_to_json(c, rumor, 0)) + return 0; + + ok = nip44_encrypt(ctx, key->secret, receiver_pubkey, + (unsigned char *)json, c->p - (unsigned char *)json-1, + c->p, cursor_remaining_capacity(c), + &b64, &b64_len); + + if (ok != NIP44_OK) { + fprintf(stderr, "rumor nip44 encrypt failed: %s\n", + nip44_err_msg(ok)); + return 0; + } + assert(b64_len % 4 == 0); + c->p = b64 + b64_len; + if (!cursor_push_byte(c, 0)) + return 0; + + fprintf(stderr, "rumor %s\n\n", json); + + /* seal data */ + seal.kind = 13; + seal.created_at = rumor->created_at; + seal.content = b64; + assert(strlen(b64) % 4 == 0); + + memcpy(seal.pubkey, key->pubkey, 32); + if (!generate_event_id(&seal, c->p, cursor_remaining_capacity(c))) + return 0; + if (!sign_event(ctx, key, &seal)) + return 0; + + json = (char*)c->p; + if (!event_to_json(c, &seal, 0)) { + fprintf(stderr, "seal -> json failed, not enough space?\n"); + return 0; + } + fprintf(stderr, "seal %.*s\n\n", (int)(c->p - (unsigned char*)json), json); + + /* encrypt seal for giftwrap contents */ + ok = nip44_encrypt(ctx, wrap_key.secret, receiver_pubkey, + (unsigned char *)json, c->p - (unsigned char *)json - 1, + c->p, cursor_remaining_capacity(c), + &b64, &b64_len); + + if (ok != NIP44_OK) { + fprintf(stderr, "seal nip44 encrypt failed: %s\n", + nip44_err_msg(ok)); + return 0; + } + + assert(b64_len % 4 == 0); + c->p = b64 + b64_len; + if (!cursor_push_byte(c, 0)) + return 0; + + giftwrap.content = b64; + + /* giftwrap data */ + json = (char*)c->p; + if (!cursor_push_hex(c, wrap_key.secret, 32)) { + fprintf(stderr, "buffer too small, bug jb55 to increase\n"); + return 0; + } + fprintf(stderr, "giftwrap_sec %.*s\n\n", 64, json); + + giftwrap.created_at = rumor->created_at; + giftwrap.kind = 1059; + memcpy(giftwrap.pubkey, wrap_key.pubkey, 32); + + json = (char*)c->p; + cursor_push_hex(c, receiver_pubkey, 32); + cursor_push_byte(c, 0); + nostr_add_tag(&giftwrap, "p", json); + + if (!generate_event_id(&giftwrap, c->p, cursor_remaining_capacity(c))) + return 0; + + json = (char*)c->p; + if (!event_to_json(c, &giftwrap, is_envelope)) { + fprintf(stderr, "buffer too small, bug jb55 to increase\n"); + return 0; + } + + printf("%s\n", json); + + free(buf); + return 1; +} + static int make_encrypted_dm(secp256k1_context *ctx, struct key *key, struct nostr_event *ev, unsigned char nostr_pubkey[32], int kind) { @@ -707,7 +852,8 @@ static int make_encrypted_dm(secp256k1_context *ctx, struct key *key, return 0; } - if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, compressed_pubkey, sizeof(compressed_pubkey))) { + if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, compressed_pubkey, + sizeof(compressed_pubkey))) { fprintf(stderr, "make_encrypted_dm: ec_pubkey_parse failed\n"); return 0; } @@ -787,6 +933,8 @@ int main(int argc, const char *argv[]) struct args args = {0}; struct nostr_event ev = {0}; struct key key; + unsigned char *buf; + size_t bufsize; secp256k1_context *ctx; if (argc < 2) @@ -827,24 +975,35 @@ int main(int argc, const char *argv[]) fprintf(stderr, "\n"); } + /* 8 MiB */ + bufsize = 2 << 22; + buf = malloc(bufsize); + if (args.flags & HAS_ENCRYPT) { int kind = args.flags & HAS_KIND? args.kind : 4; if (!make_encrypted_dm(ctx, &key, &ev, args.encrypt_to, kind)) { fprintf(stderr, "error making encrypted dm\n"); - return 0; + return 1; } + } else if (args.flags & HAS_GIFTWRAP) { + if (!make_giftwrap(ctx, &key, &ev, args.encrypt_to, + buf, bufsize, args.flags & HAS_ENVELOPE)) { + fprintf(stderr, "error making encrypted dm\n"); + return 1; + } + return 0; } // set the event's pubkey memcpy(ev.pubkey, key.pubkey, 32); if (args.flags & HAS_DIFFICULTY && !(args.flags & HAS_MINE_PUBKEY)) { - if (!mine_event(&ev, args.difficulty)) { + if (!mine_event(&ev, args.difficulty, buf, bufsize)) { fprintf(stderr, "error when mining id\n"); return 22; } } else { - if (!generate_event_id(&ev)) { + if (!generate_event_id(&ev, buf, bufsize)) { fprintf(stderr, "could not generate event id\n"); return 5; } @@ -855,11 +1014,13 @@ int main(int argc, const char *argv[]) return 6; } - if (!print_event(&ev, args.flags & HAS_ENVELOPE)) { + if (!print_event(&ev, args.flags & HAS_ENVELOPE, buf, bufsize)) { fprintf(stderr, "buffer too small\n"); return 88; } + free(buf); + return 0; } diff --git a/shell.nix b/shell.nix @@ -2,4 +2,5 @@ with pkgs; mkShell { buildInputs = [ scdoc ]; + nativeBuildInputs = [ autoreconfHook ]; }