commit d33ca522cdea5d420643c2c92a67db48764a0fc4 from: Niko Nastonen date: Mon Oct 5 18:06:50 2026 UTC Initial import commit - /dev/null commit + d33ca522cdea5d420643c2c92a67db48764a0fc4 blob - /dev/null blob + 3a6e8b483b30f62b49005802d2f385998ec37933 (mode 644) --- /dev/null +++ Makefile @@ -0,0 +1,14 @@ +PROG= ochat +SRCS= ochat.c proto.c +NOMAN= yes + +CFLAGS+= -Wall -Wextra -Wpointer-arith -Wshadow -Wmissing-prototypes +LDADD= -lcrypto -lutil +DPADD= ${LIBCRYPTO} ${LIBUTIL} + +BINDIR= /usr/local/bin +BINOWN= nobody +BINGRP= nobody +BINMODE=6555 + +.include blob - /dev/null blob + 2cf1a309829ca5ed8bb8eb9ac2f138fc5f878f97 (mode 644) --- /dev/null +++ ochat.c @@ -0,0 +1,471 @@ +/* ochat.c */ + +/* + * ochat: minimal box-to-box encrypted chat, IPv4 only. + * + * Both sides run "ochat peer-address". Each tries to connect once: + * if the peer is not up yet, it listens instead and accepts only the + * peer. One process, one TCP connection, no files, no logs. Pledges + * down to "stdio tty" before any byte from the peer is parsed. + * + * Always runs as nobody, whoever starts it: install it setuid and + * setgid nobody (mode 6555), and the very first thing it does is + * set real, effective and saved ids to nobody. Started as root, it + * also chroots to /var/empty and drops supplementary groups. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ochat.h" +#include "proto.h" + +#define PORT 7171 + +static struct termios tio_saved; +static int tio_set; +static uint8_t *pass; /* concealed, PASSMAX bytes */ + +static void cleanup(void); +static void sighandler(int); +static void droppriv(void); +static int netconnect(const struct sockaddr_in *); +static int netaccept(const struct sockaddr_in *); +static size_t readpass(void); +static void tty_restore(void); +static void sendframe(int, uint8_t, const uint8_t *, size_t); +static void printmsg(const uint8_t *, size_t); +__dead static void chat(int); + +__dead static void +usage(void) +{ + fprintf(stderr, "usage: %s [-p port] address\n", getprogname()); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + struct sockaddr_in peer; + struct rlimit rl = { 0, 0 }; + char sas[SASLEN]; + const char *errstr; + int ch, init, on = 1, port = PORT, s; + size_t passlen; + + droppriv(); + + while ((ch = getopt(argc, argv, "lp:")) != -1) { + switch (ch) { + case 'p': + port = strtonum(optarg, 1, 65535, &errstr); + if (errstr != NULL) + errx(1, "port %s: %s", optarg, errstr); + break; + default: + usage(); + } + } + + argc -= optind; + argv += optind; + if (argc != 1) + usage(); + + memset(&peer, 0, sizeof(peer)); + peer.sin_len = sizeof(peer); + peer.sin_family = AF_INET; + peer.sin_port = htons(port); + + if (inet_pton(AF_INET, argv[0], &peer.sin_addr) != 1) + errx(1, "%s: not an IPv4 address", argv[0]); + + if (setrlimit(RLIMIT_CORE, &rl) == -1) + err(1, "setrlimit"); + + pass = secalloc(PASSMAX); + proto_init(); + if (atexit(cleanup) == -1) + err(1, "atexit"); + + signal(SIGINT, sighandler); + signal(SIGTERM, sighandler); + signal(SIGHUP, sighandler); + signal(SIGPIPE, SIG_IGN); + + if (pledge("stdio inet tty", NULL) == -1) + err(1, "pledge"); + + if ((passlen = readpass()) == 0) + warnx("empty passphrase: only the SAS authenticates the peer"); + + /* Whoever connects initiates the handshake. */ + if ((s = netconnect(&peer)) != -1) + init = 1; + else { + s = netaccept(&peer); + init = 0; + } + + if (setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) == -1) + err(1, "setsockopt"); + fprintf(stderr, "connected to %s\n", argv[0]); + + if (pledge("stdio tty", NULL) == -1) + err(1, "pledge"); + + proto_handshake(s, init, pass, passlen, sas, sizeof(sas)); + explicit_bzero(pass, PASSMAX); + + fprintf(stderr, "secure channel up, SAS %s\n" + "compare the SAS with your peer out of band\n", sas); + + chat(s); +} + +/* + * Allocate memory for secrets: excluded from core dumps and, + * where the memlock limit allows, from swap. + */ +void * +secalloc(size_t len) +{ + void *p = mmap(NULL, len, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON | MAP_CONCEAL, -1, 0); + + if (p == MAP_FAILED) + err(1, "mmap"); + + (void)mlock(p, len); /* best effort, swap is encrypted anyways */ + + return p; +} + +static void +cleanup(void) +{ + tty_restore(); + + if (pass != NULL) + explicit_bzero(pass, PASSMAX); + + proto_wipe(); +} + +static void +sighandler(int sig) +{ + cleanup(); + _exit(128 + sig); +} + +/* + * Become nobody for good. From a setuid/setgid nobody binary the + * effective and saved ids already are nobody, so an unprivileged + * setresuid/setresgid may copy them into the real ids too. Root + * can do it outright, plus chroot and setgroups. + */ +static void +droppriv(void) +{ + struct passwd *pw; + uid_t uid; + gid_t gid; + + if ((pw = getpwnam("nobody")) == NULL) + errx(1, "no such user: nobody"); + uid = pw->pw_uid; + gid = pw->pw_gid; + + if (geteuid() == 0) { + if (chroot("/var/empty") == -1 || chdir("/") == -1) + err(1, "chroot"); + if (setgroups(1, &gid) == -1) + err(1, "setgroups"); + } else if (geteuid() != uid) { + errx(1, "Not running as nobody: the setuid bit was ignored. " + "Install under /usr/local (doas make install) and run it " + "from there; /home, /tmp, and /var are mounted nosuid."); + } + + if (setresgid(gid, gid, gid) == -1 || setresuid(uid, uid, uid) == -1) + errx(1, "cannot become nobody: install ochat setuid and " + "setgid nobody (chown nobody:nobody, chmod 6555)"); + + if (getuid() != uid || geteuid() != uid || getgid() != gid || getegid() != gid) + errx(1, "failed to become nobody"); + + /* We do pledge, which grants no file access at all, later, but still. */ + if (unveil("/", "") == -1 || unveil(NULL, NULL) == -1) + err(1, "unveil"); +} + +/* Try once. Refused means the peer is not up yet: return -1. */ +static int +netconnect(const struct sockaddr_in *peer) +{ + int s; + + if ((s = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) + err(1, "socket"); + if (connect(s, (const struct sockaddr *)peer, sizeof(*peer)) == 0) + return s; + if (errno != ECONNREFUSED) + err(1, "connect"); + + close(s); + + return -1; +} + +/* Listen on the port, accept only the peer, stop listening. */ +static int +netaccept(const struct sockaddr_in *peer) +{ + struct sockaddr_in sin; + socklen_t len; + int ls, on = 1, s; + + memset(&sin, 0, sizeof(sin)); + sin.sin_len = sizeof(sin); + sin.sin_family = AF_INET; + sin.sin_port = peer->sin_port; + sin.sin_addr.s_addr = htonl(INADDR_ANY); + + if ((ls = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) + err(1, "socket"); + if (setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) + err(1, "setsockopt"); + if (bind(ls, (struct sockaddr *)&sin, sizeof(sin)) == -1) + err(1, "bind"); + if (listen(ls, 1) == -1) + err(1, "listen"); + + fprintf(stderr, "waiting for peer\n"); + + for (;;) { + len = sizeof(sin); + s = accept4(ls, (struct sockaddr *)&sin, &len, SOCK_CLOEXEC); + if (s == -1) { + if (errno == EINTR || errno == ECONNABORTED) + continue; + err(1, "accept"); + } + + if (sin.sin_addr.s_addr == peer->sin_addr.s_addr) + break; + + close(s); /* not our peer: drop silently */ + } + + close(ls); + + return s; +} + +/* + * Read the passphrase from stdin with echo off. + */ +static size_t +readpass(void) +{ + struct termios tio; + size_t len = 0; + ssize_t n; + char c; + + if (isatty(STDIN_FILENO)) { + if (tcgetattr(STDIN_FILENO, &tio_saved) == -1) + err(1, "tcgetattr"); + tio = tio_saved; + tio.c_lflag &= ~ECHO; + tio_set = 1; + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio) == -1) + err(1, "tcsetattr"); + } + + fputs("passphrase: ", stderr); + + for (;;) { + if ((n = read(STDIN_FILENO, &c, 1)) == -1) { + if (errno == EINTR) + continue; + err(1, "read"); + } + if (n == 0 || c == '\n') + break; + if (len == PASSMAX) + errx(1, "passphrase too long"); + + pass[len++] = c; + } + + explicit_bzero(&c, sizeof(c)); + tty_restore(); + fputc('\n', stderr); + + return len; +} + +static void +tty_restore(void) +{ + if (tio_set) { + (void)tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio_saved); + tio_set = 0; + } +} + +void +readfull(int fd, void *buf, size_t len) +{ + uint8_t *p = buf; + ssize_t n; + + while (len > 0) { + if ((n = read(fd, p, len)) == -1) { + if (errno == EINTR) + continue; + err(1, "read"); + } + if (n == 0) + errx(1, "connection closed"); + + p += n; + len -= n; + } +} + +void +writefull(int fd, const void *buf, size_t len) +{ + const uint8_t *p = buf; + ssize_t n; + + while (len > 0) { + if ((n = write(fd, p, len)) == -1) { + if (errno == EINTR) + continue; + err(1, "write"); + } + + p += n; + len -= n; + } +} + +static void +sendframe(int s, uint8_t type, const uint8_t *msg, size_t len) +{ + uint8_t frame[FRAME]; + + proto_seal(frame, type, msg, len); + writefull(s, frame, sizeof(frame)); +} + +/* + * Print a peer message with every non-printable byte escaped, so + * the peer cannot drive our terminal. Bypasses stdio so no copy + * of the plaintext lingers in a FILE buffer. + */ +static void +printmsg(const uint8_t *msg, size_t len) +{ + char buf[2 + 4 * MSGMAX + 2]; + int n; + + buf[0] = '<'; + buf[1] = ' '; + n = strvisx(buf + 2, (const char *)msg, len, + VIS_CSTYLE | VIS_OCTAL | VIS_TAB | VIS_NL); + buf[2 + n] = '\n'; + + writefull(STDOUT_FILENO, buf, 2 + n + 1); + explicit_bzero(buf, sizeof(buf)); +} + +__dead static void +chat(int s) +{ + struct pollfd pfd[2]; + uint8_t net[FRAME], in[512], line[MSGMAX], msg[MSGMAX]; + size_t netlen = 0, linelen = 0, len; + ssize_t n; + + pfd[0].fd = STDIN_FILENO; + pfd[0].events = POLLIN; + pfd[1].fd = s; + pfd[1].events = POLLIN; + + for (;;) { + if (poll(pfd, 2, INFTIM) == -1) { + if (errno == EINTR) + continue; + err(1, "poll"); + } + + /* Peer: accumulate exactly FRAME bytes, then decrypt. */ + if (pfd[1].revents & (POLLIN | POLLHUP | POLLERR)) { + n = read(s, net + netlen, sizeof(net) - netlen); + if (n == -1 && errno != EINTR) + err(1, "read"); + if (n == 0) + errx(1, "%s", netlen ? + "truncated frame" : "peer closed connection"); + if (n > 0 && (netlen += n) == FRAME) { + netlen = 0; + if (proto_open(net, msg, &len) == T_BYE) { + fprintf(stderr, "peer left\n"); + exit(0); + } + printmsg(msg, len); + explicit_bzero(msg, sizeof(msg)); + } + } + + /* Local: split input into lines of at most MSGMAX bytes. */ + if (pfd[0].revents & (POLLIN | POLLHUP | POLLERR)) { + n = read(STDIN_FILENO, in, sizeof(in)); + if (n == -1 && errno != EINTR) + err(1, "read"); + for (int i = 0; i < n; i++) { + if (in[i] != '\n') + line[linelen++] = in[i]; + if ((in[i] == '\n' && linelen > 0) || linelen == MSGMAX) { + sendframe(s, T_MSG, line, linelen); + linelen = 0; + } + } + + explicit_bzero(in, sizeof(in)); + + if (n == 0) { + if (linelen > 0) + sendframe(s, T_MSG, line, linelen); + sendframe(s, T_BYE, NULL, 0); + explicit_bzero(line, sizeof(line)); + exit(0); + } + } + } +} blob - /dev/null blob + e11fc77b92ec22b5c2417d44483d3ce65644c580 (mode 644) --- /dev/null +++ ochat.h @@ -0,0 +1,24 @@ +/* ochat.h */ + +#ifndef OCHAT_H +#define OCHAT_H + +#include +#include + +#define MSGMAX 254 /* payload bytes per frame */ +#define PTLEN (2 + MSGMAX) /* type + len + payload = 256 */ +#define TAGLEN 16 /* poly1305 tag */ +#define FRAME (PTLEN + TAGLEN) /* bytes on the wire, always */ +#define PASSMAX 256 +#define SASLEN 20 /* XXXX-XXXX-XXXX-XXXX" + NUL */ + +#define T_MSG 1 +#define T_BYE 2 + +/* ochat.c */ +void *secalloc(size_t); +void readfull(int, void *, size_t); +void writefull(int, const void *, size_t); + +#endif /* OCHAT_H */ blob - /dev/null blob + e26a0b93d4b49272fcadcc45f5f63817b7d1e154 (mode 644) --- /dev/null +++ proto.c @@ -0,0 +1,312 @@ +/* proto.c */ + +/* + * Handshake, framing and key ratchet for ochat. + * + * Handshake (3 short, fixed-size messages, no lenght fields): + * + * both: e ephemeral X25519 public key (32) + * both: confirm AEAD tag over empty plaintext (16) + * + * dh = X25519(my_priv, peer_pub) + * h = SHA256("ochat-v1" || init_pub || resp_pub) + * psk = bcrypt(pbkdf(passphrase, salf = h) (zero if no pass) + * okm = HKDF-SHA256(ikm = dh || psk, salt = h, info = "ochat-v1-keys") + * -> k_i2r | k_r2i | k_confirm | sas_raw + * + * The confirm exchange proves both sides derived the same keys, i.e. + * the same DH result and the same passphrase. With no passphrase it + * only proves an unbroken DH, so the SAS must be compared out of band. + * + * Frames: ChaCha20-Poly1305 over the 256-byte plaintext, nonce is a + * 64-bit per-direction counter. A gap, replay or reorder fails the + * tag and kills the process. + * + * After each frame its direction's key is advanced by a one-way step, + * k' = HKDF(k, "ochat-ratchet"), and the old key is wiped. This gives + * forward secrecy within a session: a key seized at frame N cannot + * decrypt any earlier frame. (It is not post-compromise secure; that + * is the job of the ephemeral per-session keys.) + */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ochat.h" +#include "proto.h" + +#define KEYLEN 32 +#define PUBLEN 32 +#define PRIVLEN 32 +#define DHLEN 32 +#define HASHLEN 32 +#define NONCELEN 12 /* chacha20-poly1305 nonce */ +#define CONFIRMLEN TAGLEN /* AEAD tag over empty plaintext */ +#define SASRAW 8 /* -> 16 hex digits */ +#define OKMLEN (3 * KEYLEN + SASRAW) +#define PSKROUNDS 16 +#define KEYSLEN (2 * KEYLEN) /* k_send || k_recv */ + +/* + * The only durable key material is the concealed, mlocked page at + * st.keys. No EVP_AEAD_CTX is kept between frames: each seal/open + * builds one from st.keys and frees it, so libcrypto never holds a + * second long-lived copy. + */ +static struct { + uint8_t *keys; /* secalloc: k_send(32) || k_recv(32) */ + uint64_t send_ctr; + uint64_t recv_ctr; + int ready; +} st; + +static const EVP_AEAD * +aead(void) +{ + return EVP_aead_chacha20_poly1305(); +} + +/* Allocate and key a ChaCha20-Poly1305 context. */ +static EVP_AEAD_CTX * +aead_new(const uint8_t *key) +{ + EVP_AEAD_CTX *ctx; + + if ((ctx = EVP_AEAD_CTX_new()) == NULL) + errx(1, "AEAD alloc failed"); + if (!EVP_AEAD_CTX_init(ctx, aead(), key, KEYLEN, TAGLEN, NULL)) + errx(1, "AEAD init failed"); + + return ctx; +} + +/* 96-bit nonce: 32 zero bits then the counter, big-endian */ +static void +nonce_of(uint8_t n[NONCELEN], uint64_t ctr) +{ + memset(n, 0, NONCELEN); + + for (int i = 0; i < 8; i++) + n[NONCELEN - 1 - i] = (uint8_t)(ctr >> (8 * i)); +} + +/* Advance a direction key in place: k <- HKDF(k, "ochat-ratchet") */ +static void +ratchet(uint8_t *key) +{ + uint8_t next[KEYLEN]; + + if (!HKDF(next, KEYLEN, EVP_sha256(), key, KEYLEN, NULL, 0, + (const uint8_t *)"ochat-ratchet", 13)) + errx(1, "ratchet failed"); + + memcpy(key, next, KEYLEN); + explicit_bzero(next, sizeof(next)); +} + +/* sas_raw (8 bytes) -> "XXXX-XXXX-XXXX-XXXX" */ +static void +format_sas(char *out, size_t outlen, const uint8_t *raw) { + static const char hex[] = "0123456789ABCDEF"; + char tmp[SASLEN]; + int j = 0; + + for (int i = 0; i < SASRAW; i++) { + tmp[j++] = hex[raw[i] >> 4]; + tmp[j++] = hex[raw[i] & 0x0f]; + + if ((i & 1) && i != SASRAW - 1) + tmp[j++] = '-'; + } + tmp[j] = '\0'; + + if (strlcpy(out, tmp, outlen) >= outlen) + errx(1, "sas buffer too small"); +} + +void proto_init(void) +{ + memset(&st, 0, sizeof(st)); + st.keys = secalloc(KEYSLEN); /* MAP_CONCEAL + mlock; wiped in proto_wipe */ +} + +void +proto_handshake(int fd, int initiator, const uint8_t *pass, size_t passlen, + char *sas, size_t saslen) +{ + uint8_t my_pub[PUBLEN], my_priv[PRIVLEN], peer_pub[PUBLEN]; + uint8_t dh[DHLEN], h[HASHLEN], psk[KEYLEN]; + uint8_t ikm[DHLEN + KEYLEN], okm[OKMLEN]; + uint8_t tbuf[8 + 2 * PUBLEN]; + uint8_t my_confirm[CONFIRMLEN], peer_confirm[CONFIRMLEN]; + uint8_t nonce_i[NONCELEN], nonce_r[NONCELEN], scratch[1]; + const uint8_t *ksend, *krecv; + const char *my_ad, *peer_ad; + const uint8_t *my_nonce, *peer_nonce; + EVP_AEAD_CTX *cc; + size_t outlen; + + if (saslen < SASLEN) + errx(1, "sas buffer too small"); + + /* Ephemeral keypair, exchange public keys (32 bytes won't block). */ + X25519_keypair(my_pub, my_priv); + writefull(fd, my_pub, PUBLEN); + readfull(fd, peer_pub, PUBLEN); + + /* Diffie-Hellman; LibreSSL returns 0 on an all-zero shared key. */ + if (!X25519(dh, my_priv, peer_pub)) + errx(1, "X25519: degenerate peer key"); + + /* Transcript hash over both public keys in role order. */ + memcpy(tbuf, "ochat-v1", 8); + memcpy(tbuf + 8, initiator ? my_pub : peer_pub, PUBLEN); + memcpy(tbuf + 8 + PUBLEN, initiator ? peer_pub : my_pub, PUBLEN); + SHA256(tbuf, sizeof(tbuf), h); + + /* Passphrase -> PSK, salted by the transcript hash. */ + if (passlen > 0) { + if (bcrypt_pbkdf((const char *)pass, passlen, h, HASHLEN, + psk, KEYLEN, PSKROUNDS) != 0) + errx(1, "bcrypt_pbkdf failed"); + } else + memset(psk, 0, KEYLEN); + + /* HKDF(dh || psk) -> directional keys, confirm key, SAS */ + memcpy(ikm, dh, DHLEN); + memcpy(ikm + DHLEN, psk, KEYLEN); + + if (!HKDF(okm, OKMLEN, EVP_sha256(), ikm, sizeof(ikm), h, HASHLEN, + (const uint8_t *)"ochat-v1-keys", 13)) + errx(1, "HKDF failed"); + + /* Copy the per-direction keys into the concealed page. */ + ksend = initiator ? okm : okm + KEYLEN; + krecv = initiator ? okm + KEYLEN : okm; + memcpy(st.keys, ksend, KEYLEN); + memcpy(st.keys + KEYLEN, krecv, KEYLEN); + st.send_ctr = st.recv_ctr = 0; + + /* Mutual key confirmation: role-tagged AAD, distinct nonces. */ + cc = aead_new(okm + 2 * KEYLEN); + nonce_of(nonce_i, 0); + nonce_of(nonce_r, 1); + my_ad = initiator ? "ochat-confirm-i" : "ochat-confirm-r"; + peer_ad = initiator ? "ochat-confirm-r" : "ochat-confirm-i"; + my_nonce = initiator ? nonce_i : nonce_r; + peer_nonce = initiator ? nonce_r : nonce_i; + + if (!EVP_AEAD_CTX_seal(cc, my_confirm, &outlen, CONFIRMLEN, + my_nonce, NONCELEN, NULL, 0, + (const uint8_t *)my_ad, strlen(my_ad))) + errx(1, "confirm seal failed"); + + writefull(fd, my_confirm, CONFIRMLEN); + readfull(fd, peer_confirm, CONFIRMLEN); + + if (!EVP_AEAD_CTX_open(cc, scratch, &outlen, sizeof(scratch), + peer_nonce, NONCELEN, peer_confirm, CONFIRMLEN, + (const uint8_t *)peer_ad, strlen(peer_ad))) + errx(1, "peer authentication failed"); + + EVP_AEAD_CTX_free(cc); + + /* Short authentication string for out-of-band comparison. */ + format_sas(sas, saslen, okm + 3 * KEYLEN); + + st.ready = 1; + + explicit_bzero(my_priv, sizeof(my_priv)); + explicit_bzero(dh, sizeof(dh)); + explicit_bzero(psk, sizeof(psk)); + explicit_bzero(ikm, sizeof(ikm)); + explicit_bzero(okm, sizeof(okm)); +} + +void +proto_seal(uint8_t *frame, uint8_t type, const uint8_t *msg, size_t len) +{ + EVP_AEAD_CTX *ctx; + uint8_t pt[PTLEN], nonce[NONCELEN]; + size_t outlen; + + if (!st.ready) + errx(1, "seal before handshake"); + if (len > MSGMAX) + errx(1, "message to long"); + if (st.send_ctr == UINT64_MAX) + errx(1, "send counter exhausted"); + + pt[0] = type; + pt[1] = (uint8_t)len; + if (len > 0) + memcpy(pt + 2, msg, len); + memset(pt + 2 + len, 0, MSGMAX - len); + + nonce_of(nonce, st.send_ctr); + ctx = aead_new(st.keys); + + if (!EVP_AEAD_CTX_seal(ctx, frame, &outlen, FRAME, + nonce, NONCELEN, pt, PTLEN, NULL, 0) || outlen != FRAME) + errx(1, "seal failed"); + + EVP_AEAD_CTX_free(ctx); + st.send_ctr++; + ratchet(st.keys); + explicit_bzero(pt, sizeof(pt)); +} + +uint8_t +proto_open(const uint8_t *frame, uint8_t *msg, size_t *len) +{ + EVP_AEAD_CTX *ctx; + uint8_t pt[PTLEN], nonce[NONCELEN], type; + size_t outlen; + + if (!st.ready) + errx(1, "open before handshake"); + if (st.recv_ctr == UINT64_MAX) + errx(1, "recv counter exhausted"); + + nonce_of(nonce, st.recv_ctr); + ctx = aead_new(st.keys + KEYLEN); + + if (!EVP_AEAD_CTX_open(ctx, pt, &outlen, PTLEN, + nonce, NONCELEN, frame, FRAME, NULL, 0) || outlen != PTLEN) + errx(1, "decryption failed"); /* bad tag, replay or reorder */ + + EVP_AEAD_CTX_free(ctx); + st.recv_ctr++; + ratchet(st.keys + KEYLEN); + + type = pt[0]; + *len = pt[1]; + if (*len > MSGMAX) + errx(1, "invalid message length"); + if (*len > 0) + memcpy(msg, pt + 2, *len); + + explicit_bzero(pt, sizeof(pt)); + + return type; +} + +void +proto_wipe(void) +{ + if (st.keys != NULL) + explicit_bzero(st.keys, KEYSLEN); + + st.send_ctr = 0; + st.recv_ctr = 0; + st.ready = 0; +} blob - /dev/null blob + b93dfbe50dd2b6ff9b48dbd5561362ae9692027b (mode 644) --- /dev/null +++ proto.h @@ -0,0 +1,24 @@ +/* proto.h */ + +#ifndef OCHAT_PROTO_H +#define OCHAT_PROTO_H + +#include +#include + +/* + * Secure channel: an NNpsk0-style X25519 handshake with optional + * passphrase PSK and a short authentication string, then fixed-size + * ChaCha20-Poly1305 frames with an implicit per-direction counter. + * + * Every failure is fatal (errx): a bad tag, a replay, a reordered + * frame or a failed peer confirmation all tear the process down. + */ + +void proto_init(void); +void proto_handshake(int, int, const uint8_t *, size_t, char *, size_t); +void proto_seal(uint8_t *, uint8_t, const uint8_t *, size_t); +uint8_t proto_open(const uint8_t *, uint8_t *, size_t *); +void proto_wipe(void); + +#endif /* OCHAT_PROTO_H */