commit - /dev/null
commit + d33ca522cdea5d420643c2c92a67db48764a0fc4
blob - /dev/null
blob + 3a6e8b483b30f62b49005802d2f385998ec37933 (mode 644)
--- /dev/null
+++ Makefile
+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 <bsd.prog.mk>
blob - /dev/null
blob + 2cf1a309829ca5ed8bb8eb9ac2f138fc5f878f97 (mode 644)
--- /dev/null
+++ ochat.c
+/* 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 <sys/types.h>
+#include <sys/mman.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+
+#include <err.h>
+#include <errno.h>
+#include <poll.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+#include <vis.h>
+
+#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
+/* ochat.h */
+
+#ifndef OCHAT_H
+#define OCHAT_H
+
+#include <stdint.h>
+#include <stddef.h>
+
+#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
+/* 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 <sys/types.h>
+
+#include <err.h>
+#include <stdint.h>
+#include <string.h>
+#include <util.h>
+
+#include <openssl/curve25519.h>
+#include <openssl/evp.h>
+#include <openssl/hkdf.h>
+#include <openssl/sha.h>
+
+#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
+/* proto.h */
+
+#ifndef OCHAT_PROTO_H
+#define OCHAT_PROTO_H
+
+#include <stdint.h>
+#include <stddef.h>
+
+/*
+ * 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 */