aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorToni Uhlig <matzeton@googlemail.com>2020-05-22 13:43:46 +0200
committerToni Uhlig <matzeton@googlemail.com>2020-05-22 14:48:29 +0200
commitc394c09330760985d282cb866a06dea6294012aa (patch)
tree5a120d309ef25552b719844474993184a8707608
first public release
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
-rw-r--r--.clang-format77
-rw-r--r--.gitignore5
-rw-r--r--Makefile113
-rw-r--r--README.md39
-rw-r--r--client.c325
-rw-r--r--common-event2.c357
-rw-r--r--common-event2.h63
-rw-r--r--common-sodium.c106
-rw-r--r--common-sodium.h24
-rw-r--r--logging.c59
-rw-r--r--logging.h10
-rw-r--r--protocol.c599
-rw-r--r--protocol.h218
-rw-r--r--server.c368
-rw-r--r--utils.h70
15 files changed, 2433 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..cdf33a6
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,77 @@
+Language: Cpp
+BasedOnStyle : LLVM
+Standard : Cpp03
+# BasedOnStyle: LLVM
+BraceWrapping:
+ AfterClass: false
+ AfterControlStatement: false
+ AfterEnum: false
+ AfterFunction: false
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ BeforeCatch: false
+ BeforeElse: false
+ IndentBraces: true
+ConstructorInitializerIndentWidth: 4
+AlignEscapedNewlinesLeft: false
+AlignTrailingComments: true
+AllowShortBlocksOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: false
+AlwaysBreakTemplateDeclarations: true
+AlwaysBreakBeforeMultilineStrings: true
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: false
+ColumnLimit: 120
+ConstructorInitializerAllOnOneLineOrOnePerLine: true
+DerivePointerAlignment: false
+ExperimentalAutoDetectBinPacking: false
+IndentCaseLabels: true
+IndentWrappedFunctionNames: false
+IndentFunctionDeclarationAfterType: false
+MaxEmptyLinesToKeep: 1
+KeepEmptyLinesAtTheStartOfBlocks: true
+NamespaceIndentation: None
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: false
+
+PenaltyExcessCharacter : 500
+PenaltyReturnTypeOnItsOwnLine : 120
+PenaltyBreakBeforeFirstCallParameter : 100
+PenaltyBreakString : 20
+PenaltyBreakComment : 10
+PenaltyBreakFirstLessLess : 0
+
+SpacesBeforeTrailingComments: 1
+Cpp11BracedListStyle: true
+IndentWidth: 4
+TabWidth: 4
+UseTab: Never
+BreakBeforeBraces: Linux
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpacesInAngles : false
+SpaceInEmptyParentheses : false
+SpacesInCStyleCastParentheses : false
+SpaceAfterCStyleCast : false
+SpacesInContainerLiterals : true
+SpaceBeforeAssignmentOperators : true
+ContinuationIndentWidth : 4
+SpaceBeforeParens : ControlStatements
+DisableFormat : false
+AccessModifierOffset : -4
+PointerAlignment : Middle
+AlignAfterOpenBracket : Align
+AllowAllParametersOfDeclarationOnNextLine : true
+BinPackArguments : false
+BinPackParameters : false
+AlignOperands : true
+AlignConsecutiveAssignments : false
+AllowShortFunctionsOnASingleLine : None
+BreakBeforeBinaryOperators : None
+AlwaysBreakAfterReturnType : None
+SortIncludes : false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c98e71
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/client
+/server
+*.so
+*.o
+*.swp
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a11d337
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,113 @@
+CC = gcc
+INSTALL = install
+STRIP = strip
+MKDIR = mkdir
+PKG_CONFIG_BIN = pkg-config
+PREFIX = /usr
+
+ifneq ($(strip $(ENABLE_SANITIZER)),)
+ifeq ($(strip $(ENABLE_STATIC)),)
+SANITIZER_CFLAGS = -fsanitize=address -fsanitize=leak -fsanitize=undefined
+endif
+endif
+
+ifneq ($(strip $(ENABLE_DEBUG)),)
+DEBUG_CFLAGS = -Og -g3 -DDEBUG_BUILD
+EXTRA_CFLAGS =
+else
+DEBUG_CFLAGS =
+EXTRA_CFLAGS = -Werror -Os
+endif
+
+CFLAGS = -Wall -Wextra -std=gnu11 $(EXTRA_CFLAGS) -D_GNU_SOURCE $(DEBUG_CFLAGS) $(SANITIZER_CFLAGS) \
+ $(shell $(PKG_CONFIG_BIN) --cflags libsodium) \
+ $(shell $(PKG_CONFIG_BIN) --cflags libevent)
+LDFLAGS =
+
+HEADER_TARGETS = common-event2.h common-sodium.h logging.h protocol.h
+BUILD_TARGETS = common-event2.o common-sodium.o logging.o protocol.o
+
+SO_NAME=libsodium-tcp.so
+APP_HEADER_TARGETS = $(HEADER_TARGETS)
+APP_BUILD_TARGETS = $(BUILD_TARGETS)
+
+ifneq ($(strip $(ENABLE_STATIC)),)
+ifneq ($(strip $(ENABLE_SHARED)),)
+$(error ENABLE_STATIC and ENABLE_SHARED can not be used together!)
+endif
+endif
+
+ifneq ($(strip $(ENABLE_STATIC)),)
+EXTRA_CFLAGS += -static
+LDFLAGS += -pthread
+LIBS = $(shell $(PKG_CONFIG_BIN) --static --libs libsodium) \
+ $(shell $(PKG_CONFIG_BIN) --static --libs libevent)
+else
+LIBS = $(shell $(PKG_CONFIG_BIN) --libs libsodium) \
+ $(shell $(PKG_CONFIG_BIN) --libs libevent)
+endif
+
+ifneq ($(strip $(ENABLE_SHARED)),)
+SO_TARGET=$(SO_NAME)
+CFLAGS += -fPIC
+LDFLAGS = -Wl,-rpath,'$$ORIGIN:$$ORIGIN/../lib'
+SO_LDFLAGS = -shared
+APP_HEADER_TARGETS =
+APP_BUILD_TARGETS = $(SO_NAME)
+endif
+
+
+all: pre $(SO_TARGET) client server
+
+pre:
+ @echo "libsodium: $(shell $(PKG_CONFIG_BIN) --modversion --short-errors libsodium)"
+ @echo "libevent.: $(shell $(PKG_CONFIG_BIN) --modversion --short-errors libevent)"
+
+clean:
+ rm -f $(SO_NAME) client server *.o
+
+install: $(SO_TARGET) client server
+ $(MKDIR) -p '$(DESTDIR)$(PREFIX)/bin'
+ifneq ($(strip $(ENABLE_SHARED)),)
+ $(MKDIR) -p '$(DESTDIR)$(PREFIX)/lib'
+ $(INSTALL) --mode=0775 --strip --strip-program=$(STRIP) \
+ $(SO_TARGET) '$(DESTDIR)$(PREFIX)/lib'
+endif
+ $(INSTALL) --mode=0775 --strip --strip-program=$(STRIP) \
+ client server '$(DESTDIR)$(PREFIX)/bin'
+
+help:
+ @echo "usage:"
+ @echo "make \\"
+ @echo "\tENABLE_DEBUG=$(ENABLE_DEBUG) \\"
+ @echo "\tENABLE_STATIC=$(ENABLE_STATIC) \\"
+ @echo "\tENABLE_SANITIZER=$(ENABLE_SANITIZER) \\"
+ @echo "\tBUILD_STATIC=$(BUILD_STATIC) \\"
+ @echo "\tBUILD_SHARED=$(BUILD_SHARED) \\"
+ @echo "\tDESTDIR=$(DESTDIR) \\"
+ @echo "\tPREFIX=$(PREFIX)"
+ @echo "\nphony targets: pre all install clean help"
+ @echo "\nfile targets: $(SO_TARGET) client server"
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c $^ -o $@
+
+$(SO_TARGET): $(HEADER_TARGETS) $(BUILD_TARGETS)
+ $(CC) $(CFLAGS) $(SO_LDFLAGS) $(BUILD_TARGETS) $(LDFLAGS) $(LIBS) -o $@
+ifeq ($(strip $(ENABLE_DEBUG)),)
+ $(STRIP) $@
+endif
+
+client: $(APP_HEADER_TARGETS) $(APP_BUILD_TARGETS) client.c
+ $(CC) $(CFLAGS) $(APP_BUILD_TARGETS) client.c $(LDFLAGS) $(LIBS) -o $@
+ifeq ($(strip $(ENABLE_DEBUG)),)
+ $(STRIP) $@
+endif
+
+server: $(APP_HEADER_TARGETS) $(APP_BUILD_TARGETS) server.c
+ $(CC) $(CFLAGS) $(APP_BUILD_TARGETS) server.c $(LDFLAGS) $(LIBS) -o $@
+ifeq ($(strip $(ENABLE_DEBUG)),)
+ $(STRIP) $@
+endif
+
+.phony: pre all install clean help
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e44bf59
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+# Sodium TCP blueprint
+This project is the outcome of some research. It provides some blueprint/boilerplate code on how to design a TCP protocol with performance and security in mind. As the complete TCP payload is encrypted starting with the *1st* packet, a detection by **D**eep **P**acket **I**nspection engines isn't as easy as for many other proprietary or non-proprietary TCP protocols.
+It is tied to *libsodium* as cryptographic foundation and *libevent* for event based network IO. However, it should be easy to replace the *libevent* integration with something else.
+
+# build
+see `make help` for configure options
+
+Example:
+use `make ENABLE_DEBUG=y ENABLE_SANITIZER=y ENABLE_SHARED=y`
+
+to build client/server with:
+ * verbose debug logging
+ * with ASAN, LSAN and UBSAN support
+ * build code used by both, client/server, as shared library
+
+# run
+generate a private/public keypair: `./server`
+use that key: `./server -k [ServerPrivateKey]`
+connect to the server as client: `./client -k [ServerPublicKey]`
+
+other useful client/server command line arguments:
+ * `-h` set remote/listen host
+ * `-p` set remote/listen port
+ * `-f` set filepath to read/write from/to
+
+Example:
+`./server -k [ServerPrivateKey] -f /tmp/received_file`
+`./client -k [ServerPublicKey] -f /tmp/file_to_send`
+
+Send a file over the wire (client -> server).
+It is possible to use *FIFO*s as well for `-f`.
+
+## Warning
+The provided code should **not** used in production environments without further testing!
+
+## Protocol
+Simple REQUEST/RESPONSE based binary protocol. A **P**rotocol **D**ata **U**nit typically contains of a header (*struct protocol_header*) and a body (e.g. *struct protocol_data*).
+The type of **PDU** is determined in the header as well the total size of the body.
+
diff --git a/client.c b/client.c
new file mode 100644
index 0000000..38afa82
--- /dev/null
+++ b/client.c
@@ -0,0 +1,325 @@
+#include <errno.h>
+#include <event2/event.h>
+#include <event2/listener.h>
+#include <event2/buffer.h>
+#include <event2/bufferevent.h>
+#include <event2/event-config.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <signal.h>
+#include <sodium.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "common-event2.h"
+#include "common-sodium.h"
+#include "logging.h"
+#include "protocol.h"
+#include "utils.h"
+
+static struct cmd_options opts = {.key_string = NULL, .key_length = 0, .host = NULL, .port = 0, .filepath = NULL};
+static int data_fd = -1;
+
+static void send_data(struct connection * const state)
+{
+ uint8_t buf[WINDOW_SIZE];
+ ssize_t bytes_read;
+
+ if (data_fd >= 0) {
+ bytes_read = read(data_fd, buf, sizeof(buf));
+ if (bytes_read <= 0 || ev_protocol_data(state, buf, bytes_read) != 0) {
+ if (bytes_read == 0) {
+ LOG(WARNING, "EoF: Closing file descriptor %d aka %s", data_fd, opts.filepath);
+ } else {
+ LOG(WARNING, "Closing file descriptor %d aka %s: %s", data_fd, opts.filepath, strerror(errno));
+ }
+ close(data_fd);
+ data_fd = -1;
+ } else {
+ LOG(NOTICE, "Send DATA: %zd", bytes_read);
+ }
+ }
+}
+
+enum recv_return protocol_request_client_auth(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ (void)state;
+ (void)buffer;
+ (void)processed;
+ return RECV_CALLBACK_NOT_IMPLEMENTED;
+}
+
+enum recv_return protocol_request_server_helo(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_server_helo const * const helo_pkt = (struct protocol_server_helo *)buffer;
+
+ (void)processed;
+ LOG(NOTICE, "Server HELLO with message: %.*s", sizeof(helo_pkt->server_message), helo_pkt->server_message);
+
+ crypto_secretstream_xchacha20poly1305_init_pull(&state->crypto_rx_state,
+ helo_pkt->client_rx_header,
+ state->session_keys->rx);
+
+ if (ev_setup_generic_timer((struct ev_user_data *)state->user_data, PING_INTERVAL) != 0) {
+ LOG(ERROR, "Timer init failed");
+ return RECV_FATAL;
+ }
+
+ send_data(state);
+
+ state->state = CONNECTION_AUTH_SUCCESS;
+ return RECV_SUCCESS;
+}
+
+enum recv_return protocol_request_data(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_data const * const data_pkt = (struct protocol_data *)buffer;
+
+ (void)state;
+ (void)processed;
+ LOG(LP_DEBUG, "Received DATA with size: %u", data_pkt->header.body_size);
+ LOG(NOTICE, "Remote answered: %.*s", (int)data_pkt->header.body_size, data_pkt->payload);
+ send_data(state);
+ return RECV_SUCCESS;
+}
+
+enum recv_return protocol_request_ping(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_ping const * const ping_pkt = (struct protocol_ping *)buffer;
+
+ (void)processed;
+ LOG(NOTICE,
+ "Received PING with timestamp: %.*s / %lluus",
+ sizeof(ping_pkt->timestamp),
+ ping_pkt->timestamp,
+ state->last_ping_recv_usec);
+ if (state->latency_usec > 0.0) {
+ LOG(NOTICE, "PING-PONG latency: %.02lfms", state->latency_usec / 1000.0);
+ }
+
+ if (ev_protocol_pong(state) != 0) {
+ return RECV_FATAL;
+ } else {
+ return RECV_SUCCESS;
+ }
+}
+
+enum recv_return protocol_request_pong(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_pong const * const pong_pkt = (struct protocol_pong *)buffer;
+ (void)processed;
+ LOG(NOTICE,
+ "Received PONG with timestamp: %.*s / %lluus / %zu outstanding PONG's",
+ sizeof(pong_pkt->timestamp),
+ pong_pkt->timestamp,
+ state->last_pong_recv_usec,
+ state->awaiting_pong);
+
+ return RECV_SUCCESS;
+}
+
+void on_disconnect(struct connection * const state)
+{
+ struct ev_user_data * const user_data = (struct ev_user_data *)state->user_data;
+
+ if (user_data != NULL) {
+ event_base_loopexit(bufferevent_get_base(user_data->bev), NULL);
+ }
+}
+
+static void event_cb(struct bufferevent * bev, short events, void * con)
+{
+ struct connection * const c = (struct connection *)con;
+ char events_string[64] = {0};
+
+ ev_events_to_string(events, events_string, sizeof(events_string));
+ LOG(LP_DEBUG, "Event(s): 0x%02X (%s)", events, events_string);
+
+ if (events & BEV_EVENT_ERROR) {
+ LOG(ERROR, "Error from bufferevent: %s", strerror(errno));
+ on_disconnect(c);
+ return;
+ }
+ if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
+ LOG(NOTICE, "Remote end closed the connection");
+ on_disconnect(c);
+ return;
+ }
+ if (events & BEV_EVENT_CONNECTED) {
+ if (c->state != CONNECTION_NEW) {
+ LOG(ERROR, "Remote authenticated again?!");
+ return;
+ }
+ LOG(NOTICE, "Connected, sending AUTH");
+ if (generate_session_keypair_sodium(c) != 0) {
+ LOG(ERROR, "Client session keypair generation failed");
+ on_disconnect(c);
+ return;
+ }
+ if (ev_protocol_client_auth(c, "username", "passphrase") != 0) {
+ LOG(ERROR, "Client AUTH failed");
+ on_disconnect(c);
+ return;
+ }
+ }
+ if (events & EV_TIMEOUT) {
+ LOG(NOTICE, "Timeout");
+ bufferevent_enable(bev, EV_READ | EV_WRITE);
+ on_disconnect(c);
+ return;
+ }
+}
+
+static void cleanup(struct event_base ** const ev_base,
+ struct event ** const ev_sig,
+ struct longterm_keypair ** const my_keypair)
+{
+ if (*my_keypair != NULL) {
+ sodium_memzero((*my_keypair)->secretkey, crypto_kx_SECRETKEYBYTES);
+ free(*my_keypair);
+ }
+ if (*ev_sig != NULL) {
+ event_free(*ev_sig);
+ }
+ if (*ev_base != NULL) {
+ event_base_free(*ev_base);
+ }
+ *my_keypair = NULL;
+ *ev_sig = NULL;
+ *ev_base = NULL;
+}
+
+__attribute__((noreturn)) static void cleanup_and_exit(struct event_base ** const ev_base,
+ struct event ** const ev_sig,
+ struct longterm_keypair ** const my_keypair,
+ struct connection ** const state,
+ int exit_code)
+{
+ LOG(LP_DEBUG, "Cleanup and exit with exit code: %d", exit_code);
+ *state = NULL;
+ cleanup(ev_base, ev_sig, my_keypair);
+ exit(exit_code);
+}
+
+int main(int argc, char ** argv)
+{
+ struct event_base * ev_base = NULL;
+ struct event * ev_sig = NULL;
+ struct bufferevent * bev;
+ struct sockaddr_in sin;
+ struct longterm_keypair * my_keypair = NULL;
+ struct connection * c = NULL;
+
+ char ip_str[INET6_ADDRSTRLEN + 1];
+
+ parse_cmdline(&opts, argc, argv);
+ if (opts.key_string == NULL) {
+ usage(argv[0]);
+ }
+ if (opts.key_length != crypto_kx_PUBLICKEYBYTES * 2 /* hex string */) {
+ LOG(ERROR, "Invalid server public key length: %zu", opts.key_length);
+ return 1;
+ }
+ if (opts.filepath != NULL) {
+ data_fd = open(opts.filepath, O_RDONLY, 0);
+ if (data_fd < 0) {
+ LOG(ERROR, "File '%s' open() error: %s", opts.filepath, strerror(errno));
+ return 1;
+ }
+ }
+ if (opts.port <= 0 || opts.port > 65535) {
+ LOG(ERROR, "Invalid port: %d", opts.port);
+ return 2;
+ }
+
+ srandom(time(NULL));
+
+ if (sodium_init() != 0) {
+ LOG(ERROR, "Sodium init failed");
+ return 3;
+ }
+
+ if (init_sockaddr_inet(&sin, opts.host, opts.port, ip_str) != 0) {
+ return 4;
+ }
+ LOG(NOTICE, "Connecting to %s:%u", ip_str, opts.port);
+
+ /* generate client keypair */
+ my_keypair = generate_keypair_sodium();
+ if (my_keypair == NULL) {
+ LOG(ERROR, "Sodium keypair generation failed");
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 5);
+ }
+ log_bin2hex_sodium("Client public key", my_keypair->publickey, sizeof(my_keypair->publickey));
+
+ /* create global connection state */
+ c = new_connection_to_server(my_keypair);
+ if (c == NULL) {
+ LOG(ERROR, "Could not create connection state");
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 6);
+ }
+
+ /* parse server public key into global connection state */
+ if (sodium_hex2bin(
+ c->peer_publickey, sizeof(c->peer_publickey), opts.key_string, opts.key_length, NULL, NULL, NULL) != 0) {
+ LOG(ERROR, "Could not parse server public key: %s", opts.key_string);
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 7);
+ }
+ log_bin2hex_sodium("Server public key", c->peer_publickey, sizeof(c->peer_publickey));
+
+ ev_base = event_base_new();
+ if (ev_base == NULL) {
+ LOG(ERROR, "Couldn't open event base");
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 8);
+ }
+
+ ev_sig = evsignal_new(ev_base, SIGINT, ev_sighandler, event_self_cbarg());
+ if (ev_sig == NULL) {
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 9);
+ }
+ if (event_add(ev_sig, NULL) != 0) {
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 10);
+ }
+
+ bev =
+ bufferevent_socket_new(ev_base, -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_UNLOCK_CALLBACKS);
+ if (bev == NULL) {
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 11);
+ }
+
+ if (ev_setup_user_data(bev, c) != 0) {
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 12);
+ }
+
+ bufferevent_setcb(bev, ev_read_cb, ev_write_cb, event_cb, c);
+ if (bufferevent_enable(bev, EV_READ | EV_WRITE) != 0) {
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 13);
+ }
+ ev_set_io_timeouts(bev);
+
+ if (bufferevent_socket_connect(bev, (struct sockaddr *)&sin, sizeof(sin)) != 0) {
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 14);
+ }
+
+ LOG(LP_DEBUG, "Event loop");
+ if (event_base_dispatch(ev_base) != 0) {
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 15);
+ }
+
+ cleanup_and_exit(&ev_base, &ev_sig, &my_keypair, &c, 0);
+}
diff --git a/common-event2.c b/common-event2.c
new file mode 100644
index 0000000..b0bfe95
--- /dev/null
+++ b/common-event2.c
@@ -0,0 +1,357 @@
+#include <event2/buffer.h>
+#include <event2/bufferevent.h>
+#include <event2/event.h>
+#include <string.h>
+#include <time.h>
+
+#include "common-event2.h"
+#include "logging.h"
+#include "protocol.h"
+
+int ev_auth_timeout(struct ev_user_data * const user_data)
+{
+ LOG(NOTICE, "Authentication timeout");
+ ev_disconnect(user_data->state);
+ return 0;
+}
+
+int ev_add_timer(struct ev_user_data * const user_data, time_t trigger_after)
+{
+ struct timeval tv;
+
+ tv.tv_sec = trigger_after;
+ tv.tv_usec = 0;
+ return event_add(user_data->generic_timer, &tv);
+}
+
+int ev_del_timer(struct ev_user_data * const user_data)
+{
+ return event_del(user_data->generic_timer);
+}
+
+static double time_passed(struct tm * const tm)
+{
+ time_t cur = time(NULL);
+ time_t chk = timegm(tm);
+
+ return difftime(cur, chk);
+}
+
+int ev_default_timeout(struct ev_user_data * const user_data)
+{
+ if (user_data->state->awaiting_pong >= MAX_AWAITING_PONG) {
+ LOG(ERROR, "Max awaiting PONG reached: %u", MAX_AWAITING_PONG);
+ ev_disconnect(user_data->state);
+ return 0;
+ }
+
+ if (time_passed(&user_data->state->last_ping_recv) > PING_INTERVAL ||
+ time_passed(&user_data->state->last_ping_send) > PING_INTERVAL) {
+ LOG(NOTICE, "Sending PING");
+ if (ev_protocol_ping(user_data->state) != RECV_SUCCESS) {
+ LOG(WARNING, "Could not send PING");
+ return 1;
+ }
+ }
+ if (ev_add_timer(user_data, random() % PING_INTERVAL) != 0) {
+ return 1;
+ }
+ return 0;
+}
+
+static void ev_generic_timer(evutil_socket_t fd, short events, void * arg)
+{
+ struct ev_user_data * const user_data = (struct ev_user_data *)arg;
+
+ (void)fd;
+ (void)events;
+
+ if ((events & EV_TIMEOUT) == 0) {
+ return;
+ }
+
+ switch (user_data->state->state) {
+ case CONNECTION_NEW:
+ ev_auth_timeout(user_data);
+ break;
+ case CONNECTION_AUTH_SUCCESS:
+ ev_default_timeout(user_data);
+ break;
+ case CONNECTION_INVALID:
+ ev_del_timer(user_data);
+ break;
+ }
+}
+
+int ev_setup_generic_timer(struct ev_user_data * const user_data, time_t trigger_after)
+{
+ if (user_data->generic_timer != NULL) {
+ event_free(user_data->generic_timer);
+ user_data->generic_timer = NULL;
+ }
+ user_data->generic_timer = event_new(bufferevent_get_base(user_data->bev), -1, 0, ev_generic_timer, user_data);
+ if (user_data->generic_timer == NULL) {
+ return 1;
+ }
+
+ return ev_add_timer(user_data, trigger_after);
+}
+
+void ev_cleanup_user_data(struct connection * const state)
+{
+ struct ev_user_data * user_data;
+
+ user_data = (struct ev_user_data *)state->user_data;
+
+ if (user_data == NULL) {
+ return;
+ }
+
+ if (user_data->generic_timer != NULL) {
+ ev_del_timer(user_data);
+ event_free(user_data->generic_timer);
+ user_data->generic_timer = NULL;
+ }
+
+ if (user_data->bev != NULL) {
+ bufferevent_decref(user_data->bev);
+ bufferevent_free(user_data->bev);
+ user_data->bev = NULL;
+ }
+
+ free(user_data);
+ state->user_data = NULL;
+}
+
+int ev_setup_user_data(struct bufferevent * const bev, struct connection * const state)
+{
+ struct ev_user_data * udata;
+
+ udata = (struct ev_user_data *)malloc(sizeof(*udata));
+ if (udata == NULL) {
+ return 1;
+ }
+
+ udata->state = state;
+ udata->bev = bev;
+ udata->generic_timer = NULL;
+ state->user_data = udata;
+
+ bufferevent_incref(bev);
+ bufferevent_setwatermark(
+ bev,
+ EV_READ | EV_WRITE,
+ (CRYPTO_BYTES_POSTAUTH > CRYPTO_BYTES_PREAUTH ? CRYPTO_BYTES_PREAUTH : CRYPTO_BYTES_POSTAUTH) +
+ sizeof(struct protocol_header),
+ (CRYPTO_BYTES_POSTAUTH < CRYPTO_BYTES_PREAUTH ? CRYPTO_BYTES_PREAUTH : CRYPTO_BYTES_POSTAUTH) * 2 +
+ sizeof(struct protocol_header) + WINDOW_SIZE);
+
+ return 0;
+}
+
+void ev_set_io_timeouts(struct bufferevent * const bev)
+{
+ struct timeval tv;
+
+ tv.tv_sec = INACTIVITY_TIMEOUT;
+ tv.tv_usec = 0;
+ bufferevent_set_timeouts(bev, &tv, &tv);
+}
+
+void ev_sighandler(evutil_socket_t fd, short events, void * arg)
+{
+ struct event * ev_signal = (struct event *)arg;
+
+ (void)fd;
+ (void)events;
+ if (ev_signal != NULL) {
+ LOG(WARNING, "Got signal %d", event_get_signal(ev_signal));
+ event_base_loopexit(event_get_base(ev_signal), NULL);
+ }
+}
+
+int ev_protocol_client_auth(struct connection * const state, const char * const user, const char * const pass)
+{
+ int result;
+ unsigned char auth_pkt_crypted[CRYPT_PACKET_SIZE_CLIENT_AUTH];
+ struct ev_user_data * user_data = (struct ev_user_data *)state->user_data;
+
+ protocol_response_client_auth(auth_pkt_crypted, state, user, pass);
+ result = evbuffer_add(bufferevent_get_output(user_data->bev), auth_pkt_crypted, sizeof(auth_pkt_crypted));
+ return result;
+}
+
+int ev_protocol_server_helo(struct connection * const state, const char * const server_message)
+{
+ int result;
+ unsigned char helo_pkt_crypted[CRYPT_PACKET_SIZE_SERVER_HELO];
+ struct ev_user_data * user_data = (struct ev_user_data *)state->user_data;
+
+ protocol_response_server_helo(helo_pkt_crypted, state, server_message);
+ result = evbuffer_add(bufferevent_get_output(user_data->bev), helo_pkt_crypted, sizeof(helo_pkt_crypted));
+ return result;
+}
+
+int ev_protocol_data(struct connection * const state, uint8_t const * const payload, uint32_t payload_size)
+{
+ int result;
+ unsigned char data_pkt_crypted[CRYPT_PACKET_SIZE_DATA + payload_size];
+ struct ev_user_data * user_data = (struct ev_user_data *)state->user_data;
+
+ protocol_response_data(data_pkt_crypted, CRYPT_PACKET_SIZE_DATA + payload_size, state, payload, payload_size);
+ result = evbuffer_add(bufferevent_get_output(user_data->bev), data_pkt_crypted, sizeof(data_pkt_crypted));
+
+ return result;
+}
+
+int ev_protocol_ping(struct connection * const state)
+{
+ int result;
+ unsigned char ping_pkt_crypted[CRYPT_PACKET_SIZE_PING];
+ char timestamp[PROTOCOL_TIME_STRLEN];
+ struct ev_user_data * user_data = (struct ev_user_data *)state->user_data;
+
+ protocol_response_ping(ping_pkt_crypted, state);
+ if (strftime(timestamp, PROTOCOL_TIME_STRLEN, "%a, %d %b %Y %T %z", &state->last_ping_send) > 0) {
+ LOG(LP_DEBUG, "Sending PING with ts %s / %lluus", timestamp, state->last_ping_send_usec);
+ }
+ result = evbuffer_add(bufferevent_get_output(user_data->bev), ping_pkt_crypted, sizeof(ping_pkt_crypted));
+ return result;
+}
+
+int ev_protocol_pong(struct connection * const state)
+{
+ int result;
+ unsigned char pong_pkt_crypted[CRYPT_PACKET_SIZE_PONG];
+ char timestamp[PROTOCOL_TIME_STRLEN];
+ struct ev_user_data * user_data = (struct ev_user_data *)state->user_data;
+
+ protocol_response_pong(pong_pkt_crypted, state);
+ if (strftime(timestamp, PROTOCOL_TIME_STRLEN, "%a, %d %b %Y %T %z", &state->last_pong_send) > 0) {
+ LOG(LP_DEBUG, "Sending PONG with ts %s / %lluus", timestamp, state->last_pong_send_usec);
+ }
+ result = evbuffer_add(bufferevent_get_output(user_data->bev), pong_pkt_crypted, sizeof(pong_pkt_crypted));
+ return result;
+}
+
+void ev_disconnect(struct connection * const state)
+{
+ LOG(LP_DEBUG, "Closing connection");
+
+ if (state == NULL) {
+ return;
+ }
+
+ on_disconnect(state);
+ ev_cleanup_user_data(state);
+
+ if (state->session_keys != NULL) {
+ sodium_memzero(state->session_keys, sizeof(*(state->session_keys)));
+ free(state->session_keys);
+ }
+
+ free(state);
+}
+
+void ev_read_cb(struct bufferevent * bev, void * connection_state)
+{
+ struct connection * const c = (struct connection *)connection_state;
+ struct evbuffer * const input = bufferevent_get_input(bev);
+
+ LOG(LP_DEBUG, "Read %d bytes", evbuffer_get_length(input));
+
+ do {
+ uint8_t * buf = evbuffer_pullup(input, -1);
+ size_t siz = evbuffer_get_length(input);
+
+ switch (process_received(c, buf, &siz)) {
+ case RECV_SUCCESS:
+ break;
+ case RECV_FATAL:
+ LOG(ERROR, "Callback Fatal");
+ ev_disconnect(c);
+ return;
+ case RECV_FATAL_UNAUTH:
+ LOG(ERROR, "Peer not authenticated");
+ ev_disconnect(c);
+ return;
+ case RECV_FATAL_CRYPTO_ERROR:
+ LOG(ERROR, "Crypto error");
+ ev_disconnect(c);
+ return;
+ case RECV_CORRUPT_PACKET:
+ LOG(ERROR, "Packet Corrupt");
+ ev_disconnect(c);
+ return;
+ case RECV_BUFFER_NEED_MORE_DATA:
+ LOG(LP_DEBUG, "No more data to read");
+#if 0
+ /* forced output buffer flushing, not required IMHO as libevent is clever though */
+ if (bufferevent_flush(bev, EV_WRITE, BEV_FLUSH) != 0) {
+ LOG(WARNING, "Could not flush output buffer");
+ }
+#endif
+ return;
+ case RECV_CALLBACK_NOT_IMPLEMENTED:
+ LOG(WARNING, "Callback not implemented");
+ ev_disconnect(c);
+ return;
+ }
+
+ LOG(LP_DEBUG, "Draining input buffer by %zu bytes", siz);
+ evbuffer_drain(input, siz);
+ } while (1);
+}
+
+void ev_write_cb(struct bufferevent * bev, void * connection_state)
+{
+ (void)connection_state;
+ if (evbuffer_get_length(bufferevent_get_output(bev)) == 0) {
+ LOG(LP_DEBUG, "No more data to write");
+ } else {
+ LOG(LP_DEBUG, "Write %d bytes", evbuffer_get_length(bufferevent_get_output(bev)));
+ }
+}
+
+static void event_to_string(char ** buffer, size_t * const buffer_size, const char * const str)
+{
+ int written = snprintf(*buffer, *buffer_size, "%s, ", str);
+ if (written > 0) {
+ *buffer += written;
+ *buffer_size -= written;
+ }
+}
+
+void ev_events_to_string(short events, char * buffer, size_t buffer_size)
+{
+ size_t orig_size = buffer_size;
+
+ if (events & EV_TIMEOUT) {
+ event_to_string(&buffer, &buffer_size, "EV_TIMEOUT");
+ }
+ if (events & EV_READ) {
+ event_to_string(&buffer, &buffer_size, "EV_TIMEOUT");
+ }
+ if (events & EV_WRITE) {
+ event_to_string(&buffer, &buffer_size, "EV_WRITE");
+ }
+ if (events & EV_SIGNAL) {
+ event_to_string(&buffer, &buffer_size, "EV_SIGNAL");
+ }
+ if (events & EV_PERSIST) {
+ event_to_string(&buffer, &buffer_size, "EV_PERSIST");
+ }
+ if (events & EV_ET) {
+ event_to_string(&buffer, &buffer_size, "EV_ET");
+ }
+ if (events & EV_FINALIZE) {
+ event_to_string(&buffer, &buffer_size, "EV_FINALIZE");
+ }
+ if (events & EV_CLOSED) {
+ event_to_string(&buffer, &buffer_size, "EV_CLOSED");
+ }
+
+ if (orig_size > buffer_size + 2) {
+ *(buffer - 2) = '\0';
+ }
+}
diff --git a/common-event2.h b/common-event2.h
new file mode 100644
index 0000000..a402e07
--- /dev/null
+++ b/common-event2.h
@@ -0,0 +1,63 @@
+#ifndef COMMON_H
+#define COMMON_H 1
+
+#include <event2/event.h>
+#include <stdint.h>
+
+struct bufferevent;
+
+#define AUTHENTICATION_TIMEOUT 10
+#define INACTIVITY_TIMEOUT 180
+#define PING_INTERVAL (INACTIVITY_TIMEOUT / 3)
+#define MAX_AWAITING_PONG 3
+
+struct ev_user_data {
+ struct event * generic_timer;
+ struct connection * state;
+ struct bufferevent * bev;
+};
+
+extern void on_disconnect(struct connection * const state);
+
+int ev_auth_timeout(struct ev_user_data * const user_data);
+
+int ev_add_timer(struct ev_user_data * const user_data, time_t trigger_after);
+
+int ev_del_timer(struct ev_user_data * const user_data);
+
+__attribute__((warn_unused_result)) int ev_setup_generic_timer(struct ev_user_data * const user_data,
+ time_t trigger_after);
+
+void ev_cleanup_user_data(struct connection * const state);
+
+__attribute__((warn_unused_result)) int ev_setup_user_data(struct bufferevent * const bev,
+ struct connection * const state);
+
+void ev_set_io_timeouts(struct bufferevent * const bev);
+
+void ev_sighandler(evutil_socket_t fd, short events, void * arg);
+
+__attribute__((warn_unused_result)) int ev_protocol_client_auth(struct connection * const state,
+ const char * const user,
+ const char * const pass);
+
+__attribute__((warn_unused_result)) int ev_protocol_server_helo(struct connection * const state,
+ const char * const server_message);
+
+__attribute__((warn_unused_result)) int ev_protocol_data(struct connection * const state,
+ uint8_t const * const payload,
+ uint32_t payload_size);
+
+__attribute__((warn_unused_result)) int ev_protocol_ping(struct connection * const state);
+
+__attribute__((warn_unused_result)) int ev_protocol_pong(struct connection * const state);
+
+void ev_disconnect(struct connection * const state);
+
+void ev_read_cb(struct bufferevent * bev, void * connection_state);
+
+void ev_write_cb(struct bufferevent * bev, void * connection_state);
+
+void ev_events_to_string(short events, char * buffer, size_t buffer_size);
+
+#endif
diff --git a/common-sodium.c b/common-sodium.c
new file mode 100644
index 0000000..4398782
--- /dev/null
+++ b/common-sodium.c
@@ -0,0 +1,106 @@
+#include <sodium.h>
+
+#include "common-sodium.h"
+#include "logging.h"
+#include "protocol.h"
+
+void log_bin2hex_sodium(char const * const prefix, uint8_t const * const buffer, size_t size)
+{
+ char hexstr[2 * size + 1];
+ LOG(NOTICE, "%s: %s", prefix, sodium_bin2hex(hexstr, sizeof(hexstr), buffer, size));
+ sodium_memzero(hexstr, sizeof(hexstr));
+}
+
+struct longterm_keypair * generate_keypair_sodium(void)
+{
+ struct longterm_keypair * keypair = (struct longterm_keypair *)malloc(sizeof(*keypair));
+
+ if (keypair == NULL) {
+ return NULL;
+ }
+
+ sodium_memzero(keypair->publickey, crypto_kx_PUBLICKEYBYTES);
+ sodium_memzero(keypair->secretkey, crypto_kx_SECRETKEYBYTES);
+ crypto_kx_keypair(keypair->publickey, keypair->secretkey);
+ sodium_mlock(keypair, sizeof(*keypair));
+
+ return keypair;
+}
+
+struct longterm_keypair * generate_keypair_from_secretkey_hexstr_sodium(char const * const secretkey_hexstr,
+ size_t secretkey_hexstr_len)
+{
+ struct longterm_keypair * keypair = (struct longterm_keypair *)malloc(sizeof(*keypair));
+
+ if (keypair == NULL) {
+ return NULL;
+ }
+
+ if (sodium_hex2bin(
+ keypair->secretkey, sizeof(keypair->secretkey), secretkey_hexstr, secretkey_hexstr_len, NULL, NULL, NULL) !=
+ 0) {
+ LOG(ERROR, "Could not parse private key: %s", secretkey_hexstr);
+ goto error;
+ }
+
+ if (crypto_scalarmult_base(keypair->publickey, keypair->secretkey) != 0) {
+ LOG(ERROR, "Could not extract public key from a secret key");
+ goto error;
+ }
+
+ return keypair;
+error:
+ free(keypair);
+ return NULL;
+}
+
+int generate_session_keypair_sodium(struct connection * const state)
+{
+ if (state->session_keys != NULL) {
+ LOG(ERROR, "Session initialization invoked twice, abort");
+ return 1;
+ }
+
+ state->session_keys = (struct session_keys *)malloc(sizeof(*(state->session_keys)));
+ if (state->session_keys == NULL) {
+ return 1;
+ }
+
+ if (state->is_server_side != 0 && crypto_kx_server_session_keys(state->session_keys->rx,
+ state->session_keys->tx,
+ state->my_keypair->publickey,
+ state->my_keypair->secretkey,
+ state->peer_publickey) != 0) {
+ LOG(ERROR, "Session key creation failed");
+ return 1;
+ } else if (state->is_server_side == 0 && crypto_kx_client_session_keys(state->session_keys->rx,
+ state->session_keys->tx,
+ state->my_keypair->publickey,
+ state->my_keypair->secretkey,
+ state->peer_publickey) != 0) {
+ LOG(ERROR, "Session key creation failed");
+ return 1;
+ }
+
+ log_bin2hex_sodium("Generated session rx key", state->session_keys->rx, crypto_kx_SESSIONKEYBYTES);
+ log_bin2hex_sodium("Generated session tx key", state->session_keys->tx, crypto_kx_SESSIONKEYBYTES);
+
+ return 0;
+}
+
+int init_sockaddr_inet(struct sockaddr_in * const sin,
+ const char * const host,
+ int port,
+ char ip_str[INET6_ADDRSTRLEN + 1])
+{
+ memset(sin, 0, sizeof(*sin));
+ sin->sin_family = AF_INET;
+ sin->sin_port = htons(port);
+ if (inet_pton(sin->sin_family, host, &sin->sin_addr) <= 0 ||
+ inet_ntop(sin->sin_family, &sin->sin_addr, ip_str, INET6_ADDRSTRLEN) == NULL) {
+ LOG(ERROR, "Invalid host: %s", host);
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/common-sodium.h b/common-sodium.h
new file mode 100644
index 0000000..95ec94d
--- /dev/null
+++ b/common-sodium.h
@@ -0,0 +1,24 @@
+#ifndef COMMON_SODIUM_H
+#define COMMON_SODIUM_H 1
+
+#include <arpa/inet.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+struct connection;
+
+void log_bin2hex_sodium(char const * const prefix, uint8_t const * const buffer, size_t size);
+
+__attribute__((warn_unused_result)) struct longterm_keypair * generate_keypair_sodium(void);
+
+__attribute__((warn_unused_result)) struct longterm_keypair * generate_keypair_from_secretkey_hexstr_sodium(
+ char const * const secretkey_hexstr, size_t secretkey_hexstr_len);
+
+__attribute__((warn_unused_result)) int generate_session_keypair_sodium(struct connection * const state);
+
+__attribute__((warn_unused_result)) int init_sockaddr_inet(struct sockaddr_in * const sin,
+ const char * const host,
+ int port,
+ char ip_str[INET6_ADDRSTRLEN + 1]);
+
+#endif
diff --git a/logging.c b/logging.c
new file mode 100644
index 0000000..aadfe50
--- /dev/null
+++ b/logging.c
@@ -0,0 +1,59 @@
+#include <assert.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "logging.h"
+
+#define LOGMSG_MAXLEN BUFSIZ
+/* ANSI terminal color codes */
+#define RESET "\x1B[0m"
+#define GRN "\x1B[32;1m"
+#define YEL "\x1B[33;1m"
+#define RED "\x1B[31;1;5m"
+#define BLU "\x1B[34;1;1m"
+#define CYA "\x1B[36;1;1m"
+#define DEF RESET
+
+#ifdef DEBUG_BUILD
+static log_priority lower_prio = LP_DEBUG;
+#else
+static log_priority lower_prio = NOTICE;
+#endif
+
+void log_fmt_colored(log_priority prio, const char * fmt, ...)
+{
+ char out[LOGMSG_MAXLEN + 1];
+ va_list arglist;
+
+ if (prio < lower_prio)
+ return;
+
+ assert(fmt);
+ va_start(arglist, fmt);
+ assert(vsnprintf(&out[0], LOGMSG_MAXLEN, fmt, arglist) >= 0);
+ va_end(arglist);
+
+ switch (prio) {
+ case LP_DEBUG:
+ printf("[" DEF "DEBUG" RESET "] %s\n", out);
+ break;
+ case NOTICE:
+ printf("[" GRN "NOTICE" RESET "] %s\n", out);
+ break;
+ case WARNING:
+ printf("[" YEL "WARNING" RESET "] %s\n", out);
+ break;
+ case ERROR:
+ printf("[" RED "ERROR" RESET "] %s\n", out);
+ break;
+ case EVENT:
+ printf("[" BLU "EVENT" RESET "] %s\n", out);
+ break;
+ case PROTO:
+ printf("[" CYA "PROTO" RESET "] %s\n", out);
+ break;
+ }
+}
diff --git a/logging.h b/logging.h
new file mode 100644
index 0000000..b9174bf
--- /dev/null
+++ b/logging.h
@@ -0,0 +1,10 @@
+#ifndef LOGGING_H
+#define LOGGING_H 1
+
+#define LOG log_fmt_colored
+
+typedef enum log_priority { LP_DEBUG = 0, NOTICE, WARNING, ERROR, EVENT, PROTO } log_priority;
+
+void log_fmt_colored(log_priority prio, const char * fmt, ...);
+
+#endif
diff --git a/protocol.c b/protocol.c
new file mode 100644
index 0000000..8f43584
--- /dev/null
+++ b/protocol.c
@@ -0,0 +1,599 @@
+#include <time.h>
+#include <sys/time.h>
+
+#include "protocol.h"
+
+#define PLAIN_PACKET_POINTER_AFTER_HEADER(pointer) ((uint8_t *)pointer + PLAIN_PACKET_HEADER_SIZE)
+#define CRYPT_PACKET_POINTER_AFTER_HEADER(pointer) ((uint8_t *)pointer + CRYPT_PACKET_HEADER_SIZE)
+
+#define PLAIN_PACKET_HEADER_BODY_SIZE(header) (header->body_size)
+#define CRYPT_PACKET_HEADER_BODY_SIZE(header) (PLAIN_PACKET_HEADER_BODY_SIZE(header) + CRYPTO_BYTES_POSTAUTH)
+
+/*****************************
+ * PDU receive functionality *
+ *****************************/
+
+struct readonly_buffer {
+ uint8_t const * const pointer;
+ size_t const size;
+};
+
+struct readwrite_buffer {
+ uint8_t * const pointer;
+ size_t const size;
+ size_t used;
+};
+
+typedef enum recv_return (*protocol_cb)(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed);
+
+const struct protocol_callback {
+ protocol_cb callback;
+} protocol_callbacks[] = {[TYPE_INVALID] = {NULL},
+ [TYPE_CLIENT_AUTH] = {protocol_request_client_auth},
+ [TYPE_SERVER_HELO] = {protocol_request_server_helo},
+ [TYPE_DATA] = {protocol_request_data},
+ [TYPE_PING] = {protocol_request_ping},
+ [TYPE_PONG] = {protocol_request_pong},
+ [TYPE_COUNT] = {NULL}};
+
+static size_t calculate_min_recv_size(enum header_types type, size_t size)
+{
+ switch (type) {
+ case TYPE_CLIENT_AUTH:
+ if (size != sizeof(struct protocol_client_auth) - sizeof(struct protocol_header)) {
+ return 0;
+ }
+ return sizeof(struct protocol_client_auth);
+ case TYPE_SERVER_HELO:
+ if (size != sizeof(struct protocol_server_helo) - sizeof(struct protocol_header)) {
+ return 0;
+ }
+ return sizeof(struct protocol_server_helo);
+ case TYPE_DATA:
+ return sizeof(struct protocol_data);
+ case TYPE_PING:
+ return sizeof(struct protocol_ping);
+ case TYPE_PONG:
+ return sizeof(struct protocol_pong);
+
+ /* required to not generate compiler warnings */
+ case TYPE_COUNT:
+ case TYPE_INVALID:
+ break;
+ }
+
+ return 0;
+}
+
+static enum recv_return parse_protocol_timestamp(char const protocol_timestamp[PROTOCOL_TIME_STRLEN],
+ struct tm * const dest)
+{
+ char timestamp_sz[PROTOCOL_TIME_STRLEN + 1];
+ strncpy(timestamp_sz, protocol_timestamp, sizeof(timestamp_sz) - 1);
+ timestamp_sz[PROTOCOL_TIME_STRLEN] = '\0';
+ if (strptime(timestamp_sz, "%a, %d %b %Y %T %z", dest) == NULL) {
+ return RECV_FATAL;
+ }
+ return RECV_SUCCESS;
+}
+
+static enum recv_return process_body(struct connection * const state,
+ struct readonly_buffer const * const encrypted,
+ struct readwrite_buffer * const decrypted,
+ size_t * const processed)
+{
+ struct protocol_header const * const hdr = (struct protocol_header *)decrypted->pointer;
+ enum recv_return retval;
+
+ (void)encrypted;
+ switch (hdr->pdu_type) {
+ case TYPE_CLIENT_AUTH: {
+ struct protocol_client_auth const * const auth_pkt = (struct protocol_client_auth *)hdr;
+
+ /* client greets us, protocol version check */
+ state->used_protocol_version = ntohl(auth_pkt->protocol_version);
+ if (state->used_protocol_version != PROTOCOL_VERSION) {
+ return RECV_FATAL;
+ }
+ memcpy(state->last_nonce, auth_pkt->nonce, crypto_box_NONCEBYTES);
+ memcpy(state->peer_publickey, auth_pkt->client_publickey, crypto_kx_PUBLICKEYBYTES);
+ *processed += CRYPT_PACKET_SIZE_CLIENT_AUTH;
+ break;
+ }
+ case TYPE_SERVER_HELO: {
+ struct protocol_server_helo const * const helo_pkt = (struct protocol_server_helo *)hdr;
+
+ /* server greets us, increment and validate nonce */
+ sodium_increment(state->last_nonce, crypto_box_NONCEBYTES);
+ if (sodium_memcmp(helo_pkt->nonce_increment, state->last_nonce, crypto_box_NONCEBYTES) != 0) {
+ return RECV_FATAL;
+ }
+ *processed += CRYPT_PACKET_SIZE_SERVER_HELO;
+ break;
+ }
+ case TYPE_DATA: {
+ *processed += CRYPT_PACKET_SIZE_DATA + PLAIN_PACKET_HEADER_BODY_SIZE(hdr);
+ break;
+ }
+ case TYPE_PING: {
+ struct protocol_ping const * const ping_pkt = (struct protocol_ping *)hdr;
+
+ retval = parse_protocol_timestamp(ping_pkt->timestamp, &state->last_ping_recv);
+ if (retval != RECV_SUCCESS) {
+ return retval;
+ }
+ state->last_ping_recv_usec = be64toh(ping_pkt->timestamp_usec);
+ *processed += CRYPT_PACKET_SIZE_PING;
+ break;
+ }
+ case TYPE_PONG: {
+ struct protocol_pong const * const pong_pkt = (struct protocol_pong *)hdr;
+
+ retval = parse_protocol_timestamp(pong_pkt->timestamp, &state->last_pong_recv);
+ if (retval != RECV_SUCCESS) {
+ return retval;
+ }
+ if (state->awaiting_pong == 0) {
+ return RECV_FATAL;
+ }
+ state->awaiting_pong--;
+ state->last_pong_recv_usec = be64toh(pong_pkt->timestamp_usec);
+ state->latency_usec = state->last_pong_recv_usec - state->last_ping_send_usec;
+ *processed += CRYPT_PACKET_SIZE_PONG;
+ break;
+ }
+ /* required to not generate compiler warnings */
+ case TYPE_COUNT:
+ case TYPE_INVALID:
+ return RECV_FATAL;
+ }
+
+ return RECV_SUCCESS;
+}
+
+static enum recv_return run_protocol_callback(struct connection * const state,
+ struct readonly_buffer const * const encrypted,
+ struct readwrite_buffer * const decrypted,
+ size_t * const processed)
+{
+ struct protocol_header const * const hdr = (struct protocol_header *)decrypted->pointer;
+ enum header_types type = (enum header_types)hdr->pdu_type;
+ size_t min_size = 0;
+ uint16_t size = hdr->body_size;
+
+ switch (state->state) {
+ case CONNECTION_INVALID:
+ return RECV_FATAL;
+ case CONNECTION_NEW:
+ /* only TYPE_CLIENT_AUTH and TYPE_SERVER_HELO allowed if CONNECTION_NEW */
+ if (type != TYPE_CLIENT_AUTH && type != TYPE_SERVER_HELO) {
+ return RECV_FATAL_UNAUTH;
+ } else {
+ break;
+ }
+ case CONNECTION_AUTH_SUCCESS:
+ break;
+ }
+
+ min_size = calculate_min_recv_size(type, size);
+ if (min_size == 0) {
+ return RECV_CORRUPT_PACKET;
+ }
+
+ if (decrypted->used < min_size) {
+ return RECV_BUFFER_NEED_MORE_DATA;
+ }
+
+ if (protocol_callbacks[type].callback == NULL) {
+ return RECV_CALLBACK_NOT_IMPLEMENTED;
+ }
+
+ if (process_body(state, encrypted, decrypted, processed) != RECV_SUCCESS) {
+ return RECV_FATAL;
+ }
+
+ return protocol_callbacks[type].callback(state, hdr, processed);
+}
+
+static void header_ntoh(struct protocol_header * const hdr)
+{
+ hdr->magic = ntohl(hdr->magic);
+ hdr->pdu_type = ntohs(hdr->pdu_type);
+ hdr->body_size = ntohs(hdr->body_size);
+}
+
+static enum recv_return validate_header(struct protocol_header const * const hdr, size_t buffer_size)
+{
+ enum header_types type = (enum header_types)hdr->pdu_type;
+ uint16_t size;
+
+ if (hdr->magic != PROTOCOL_MAGIC) {
+ return RECV_CORRUPT_PACKET;
+ }
+
+ size = hdr->body_size;
+ /* following check does not make sense if sizeof(header.body_size) == 2 and WINDOW_SIZE >= 65535 */
+#if WINDOW_SIZE < 65535
+ if (size > WINDOW_SIZE) {
+ return RECV_CORRUPT_PACKET;
+ }
+#elif WINDOW_SIZE > 65535
+#error "Remember to change that code part if you've changed the type of header.body_size e.g. to uint32_t"
+#endif
+ if (size > buffer_size) {
+ return RECV_BUFFER_NEED_MORE_DATA;
+ }
+
+ if (type <= TYPE_INVALID || type >= TYPE_COUNT) {
+ return RECV_CORRUPT_PACKET;
+ }
+
+ return RECV_SUCCESS;
+}
+
+static enum recv_return decrypt_preauth(struct connection * const state,
+ struct readonly_buffer const * const encrypted,
+ struct readwrite_buffer * const decrypted)
+{
+ enum recv_return retval;
+ struct protocol_header * hdr;
+ size_t crypted_size;
+
+ if (state->is_server_side == 1) {
+ crypted_size = CRYPT_PACKET_SIZE_CLIENT_AUTH;
+ } else {
+ crypted_size = CRYPT_PACKET_SIZE_SERVER_HELO;
+ }
+
+ if (encrypted->size < crypted_size) {
+ return RECV_BUFFER_NEED_MORE_DATA;
+ }
+ if (decrypted->used + (crypted_size - CRYPTO_BYTES_PREAUTH) > decrypted->size) {
+ return RECV_FATAL;
+ }
+
+ if (crypto_box_seal_open(decrypted->pointer,
+ encrypted->pointer,
+ crypted_size,
+ state->my_keypair->publickey,
+ state->my_keypair->secretkey) != 0) {
+
+ return RECV_FATAL_CRYPTO_ERROR;
+ }
+ decrypted->used += (crypted_size - CRYPTO_BYTES_PREAUTH);
+
+ hdr = (struct protocol_header *)decrypted->pointer;
+ header_ntoh(hdr);
+
+ retval = validate_header(hdr, decrypted->used);
+ if (retval != RECV_SUCCESS) {
+ return retval;
+ }
+
+ return RECV_SUCCESS;
+}
+
+static enum recv_return decrypt_header(struct connection * const state,
+ struct readonly_buffer const * const encrypted,
+ struct readwrite_buffer * const decrypted)
+{
+ unsigned char tag = 0;
+
+ if (encrypted->size < CRYPT_PACKET_HEADER_SIZE) {
+ return RECV_BUFFER_NEED_MORE_DATA;
+ }
+ if (decrypted->used + PLAIN_PACKET_HEADER_SIZE > decrypted->size) {
+ return RECV_FATAL;
+ }
+
+ if (state->partial_packet_received == 0 && crypto_secretstream_xchacha20poly1305_pull(&state->crypto_rx_state,
+ decrypted->pointer,
+ NULL,
+ &tag,
+ encrypted->pointer,
+ CRYPT_PACKET_HEADER_SIZE,
+ NULL,
+ 0) != 0) {
+ return RECV_FATAL_CRYPTO_ERROR;
+ }
+
+ if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
+ return RECV_FATAL;
+ }
+
+ decrypted->used += PLAIN_PACKET_HEADER_SIZE;
+ return RECV_SUCCESS;
+}
+
+static enum recv_return decrypt_body(struct connection * const state,
+ struct protocol_header const * const hdr,
+ struct readonly_buffer const * const encrypted,
+ struct readwrite_buffer * const decrypted)
+{
+ unsigned char tag = 0;
+
+ if (encrypted->size < CRYPT_PACKET_HEADER_BODY_SIZE(hdr) + CRYPT_PACKET_HEADER_SIZE) {
+ return RECV_BUFFER_NEED_MORE_DATA;
+ }
+ if (decrypted->used + PLAIN_PACKET_HEADER_BODY_SIZE(hdr) > decrypted->size) {
+ return RECV_FATAL;
+ }
+
+ if (crypto_secretstream_xchacha20poly1305_pull(&state->crypto_rx_state,
+ PLAIN_PACKET_POINTER_AFTER_HEADER(decrypted->pointer),
+ NULL,
+ &tag,
+ CRYPT_PACKET_POINTER_AFTER_HEADER(encrypted->pointer),
+ CRYPT_PACKET_HEADER_BODY_SIZE(hdr),
+ NULL,
+ 0) != 0) {
+ return RECV_FATAL_CRYPTO_ERROR;
+ }
+
+ if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
+ return RECV_FATAL;
+ }
+
+ decrypted->used += PLAIN_PACKET_HEADER_BODY_SIZE(hdr);
+ return RECV_SUCCESS;
+}
+
+static enum recv_return decrypt_postauth(struct connection * const state,
+ struct readonly_buffer const * const encrypted,
+ struct readwrite_buffer * const decrypted)
+{
+ enum recv_return retval;
+ struct protocol_header * hdr;
+
+ retval = decrypt_header(state, encrypted, decrypted);
+ if (retval != RECV_SUCCESS) {
+ return retval;
+ }
+
+ hdr = (struct protocol_header *)decrypted->pointer;
+ if (state->partial_packet_received != 0) {
+ *hdr = state->partial_packet_header;
+ } else {
+ header_ntoh(hdr);
+ }
+
+ retval = validate_header(hdr, decrypted->used);
+ if (retval != RECV_SUCCESS && retval != RECV_BUFFER_NEED_MORE_DATA) {
+ return retval;
+ }
+
+ retval = decrypt_body(state, hdr, encrypted, decrypted);
+ if (retval != RECV_SUCCESS) {
+ return retval;
+ }
+
+ return RECV_SUCCESS;
+}
+
+enum recv_return process_received(struct connection * const state,
+ uint8_t const * const buffer,
+ size_t * const buffer_size)
+{
+ uint8_t decrypted_buffer[PLAIN_PACKET_HEADER_SIZE + WINDOW_SIZE];
+ struct readonly_buffer const encrypted = {.pointer = buffer, .size = *buffer_size};
+ struct readwrite_buffer decrypted = {.pointer = &decrypted_buffer[0], .size = sizeof(decrypted_buffer), .used = 0};
+
+ switch (state->state) {
+ case CONNECTION_INVALID:
+ return RECV_FATAL;
+ case CONNECTION_NEW: {
+ enum recv_return retval = decrypt_preauth(state, &encrypted, &decrypted);
+ if (retval != RECV_SUCCESS) {
+ return retval;
+ }
+ break;
+ }
+ case CONNECTION_AUTH_SUCCESS: {
+ enum recv_return retval = decrypt_postauth(state, &encrypted, &decrypted);
+ if (retval != RECV_SUCCESS) {
+ if (retval == RECV_BUFFER_NEED_MORE_DATA) {
+ if (decrypted.used == PLAIN_PACKET_HEADER_SIZE) {
+ state->partial_packet_received = 1;
+ state->partial_packet_header = *(struct protocol_header *)decrypted_buffer;
+ } else if (decrypted.used != 0) {
+ return RECV_CORRUPT_PACKET;
+ }
+ }
+ return retval;
+ }
+ break;
+ }
+ }
+
+ state->partial_packet_received = 0;
+ *buffer_size = 0;
+ return run_protocol_callback(state, &encrypted, &decrypted, buffer_size);
+}
+
+/**************************
+ * PDU send functionality *
+ **************************/
+
+static void protocol_response(void * const buffer, uint16_t body_and_payload_size, enum header_types type)
+{
+ struct protocol_header * const header = (struct protocol_header *)buffer;
+
+ header->magic = htonl(PROTOCOL_MAGIC);
+ header->pdu_type = htons((uint16_t)type);
+ header->body_size = htons(body_and_payload_size - sizeof(*header));
+}
+
+void protocol_response_client_auth(unsigned char out[CRYPT_PACKET_SIZE_CLIENT_AUTH],
+ struct connection * const state,
+ const char * const user,
+ const char * const pass)
+{
+ struct protocol_client_auth auth_pkt;
+
+ protocol_response(&auth_pkt, sizeof(auth_pkt), TYPE_CLIENT_AUTH);
+ /* version */
+ state->used_protocol_version = PROTOCOL_VERSION;
+ auth_pkt.protocol_version = htonl(state->used_protocol_version);
+ /* nonce */
+ randombytes_buf(state->last_nonce, crypto_box_NONCEBYTES);
+ memcpy(auth_pkt.nonce, state->last_nonce, crypto_box_NONCEBYTES);
+ /* keys required by server */
+ memcpy(auth_pkt.client_publickey, state->my_keypair->publickey, crypto_kx_PUBLICKEYBYTES);
+ /* login credentials */
+ randombytes_buf(&auth_pkt.login, sizeof(auth_pkt.login));
+ randombytes_buf(&auth_pkt.passphrase, sizeof(auth_pkt.passphrase));
+ strncpy(auth_pkt.login, user, sizeof(auth_pkt.login));
+ strncpy(auth_pkt.passphrase, pass, sizeof(auth_pkt.passphrase));
+ /* setup secretstream header for server_rx */
+ crypto_secretstream_xchacha20poly1305_init_push(&state->crypto_tx_state,
+ auth_pkt.server_rx_header,
+ state->session_keys->tx);
+ /* encrypt */
+ crypto_box_seal(out, (uint8_t *)&auth_pkt, sizeof(auth_pkt), state->peer_publickey);
+}
+
+void protocol_response_server_helo(unsigned char out[CRYPT_PACKET_SIZE_SERVER_HELO],
+ struct connection * const state,
+ const char * const welcome_message)
+{
+ struct protocol_server_helo helo_pkt;
+
+ protocol_response(&helo_pkt, sizeof(helo_pkt), TYPE_SERVER_HELO);
+ /* nonce */
+ sodium_increment(state->last_nonce, crypto_box_NONCEBYTES);
+ memcpy(helo_pkt.nonce_increment, state->last_nonce, crypto_box_NONCEBYTES);
+ /* server messgae */
+ strncpy(helo_pkt.server_message, welcome_message, sizeof(helo_pkt.server_message));
+ /* setup secretstream header for client_rx */
+ crypto_secretstream_xchacha20poly1305_init_push(&state->crypto_tx_state,
+ helo_pkt.client_rx_header,
+ state->session_keys->tx);
+ /* encrypt */
+ crypto_box_seal(out, (uint8_t *)&helo_pkt, sizeof(helo_pkt), state->peer_publickey);
+}
+
+void protocol_response_data(uint8_t * out,
+ size_t const out_size,
+ struct connection * const state,
+ uint8_t const * const payload,
+ size_t payload_size)
+{
+ struct protocol_header data_hdr;
+
+ if (out_size != CRYPT_PACKET_SIZE_DATA + payload_size) {
+ return;
+ }
+ protocol_response(&data_hdr, sizeof(data_hdr) + payload_size, TYPE_DATA);
+
+ crypto_secretstream_xchacha20poly1305_push(
+ &state->crypto_tx_state, out, NULL, (uint8_t *)&data_hdr, sizeof(data_hdr), NULL, 0, 0);
+ crypto_secretstream_xchacha20poly1305_push(
+ &state->crypto_tx_state, CRYPT_PACKET_POINTER_AFTER_HEADER(out), NULL, payload, payload_size, NULL, 0, 0);
+}
+
+static int create_timestamp(struct tm * const timestamp,
+ char timestamp_str[PROTOCOL_TIME_STRLEN],
+ suseconds_t * const usec)
+{
+ time_t ts;
+ struct timeval ts_val;
+
+ gettimeofday(&ts_val, NULL);
+ *usec = ts_val.tv_usec;
+ ts = time(NULL);
+ gmtime_r(&ts, timestamp);
+ return strftime(timestamp_str, PROTOCOL_TIME_STRLEN, "%a, %d %b %Y %T %z", timestamp);
+}
+
+void protocol_response_ping(unsigned char out[CRYPT_PACKET_SIZE_PING], struct connection * const state)
+{
+ struct protocol_ping ping_pkt;
+
+ state->awaiting_pong++;
+ protocol_response(&ping_pkt, sizeof(ping_pkt), TYPE_PING);
+ create_timestamp(&state->last_ping_send, ping_pkt.timestamp, &state->last_ping_send_usec);
+ ping_pkt.timestamp_usec = htobe64(state->last_ping_send_usec);
+
+ crypto_secretstream_xchacha20poly1305_push(
+ &state->crypto_tx_state, &out[0], NULL, (uint8_t *)&ping_pkt.header, sizeof(ping_pkt.header), NULL, 0, 0);
+ crypto_secretstream_xchacha20poly1305_push(&state->crypto_tx_state,
+ CRYPT_PACKET_POINTER_AFTER_HEADER(&out[0]),
+ NULL,
+ (uint8_t *)&ping_pkt.header + PLAIN_PACKET_HEADER_SIZE,
+ PLAIN_PACKET_BODY_SIZE(struct protocol_ping),
+ NULL,
+ 0,
+ 0);
+}
+
+void protocol_response_pong(unsigned char out[CRYPT_PACKET_SIZE_PONG], struct connection * const state)
+{
+ struct protocol_pong pong_pkt;
+
+ protocol_response(&pong_pkt, sizeof(pong_pkt), TYPE_PONG);
+ create_timestamp(&state->last_pong_send, pong_pkt.timestamp, &state->last_pong_send_usec);
+ pong_pkt.timestamp_usec = htobe64(state->last_pong_send_usec);
+
+ crypto_secretstream_xchacha20poly1305_push(
+ &state->crypto_tx_state, &out[0], NULL, (uint8_t *)&pong_pkt.header, sizeof(pong_pkt.header), NULL, 0, 0);
+ crypto_secretstream_xchacha20poly1305_push(&state->crypto_tx_state,
+ CRYPT_PACKET_POINTER_AFTER_HEADER(&out[0]),
+ NULL,
+ (uint8_t *)&pong_pkt.header + PLAIN_PACKET_HEADER_SIZE,
+ PLAIN_PACKET_BODY_SIZE(struct protocol_pong),
+ NULL,
+ 0,
+ 0);
+}
+
+/**********************************
+ * connection state functionality *
+ **********************************/
+
+static struct connection * new_connection(struct longterm_keypair const * const my_keypair)
+{
+ struct connection * c = (struct connection *)malloc(sizeof(*c));
+
+ if (c == NULL) {
+ return NULL;
+ }
+ c->state = CONNECTION_NEW;
+ c->awaiting_pong = 0;
+ c->session_keys = NULL;
+ c->my_keypair = my_keypair;
+ c->user_data = NULL;
+ create_timestamp(&c->last_ping_recv, NULL, &c->last_ping_recv_usec);
+ create_timestamp(&c->last_ping_send, NULL, &c->last_ping_send_usec);
+ create_timestamp(&c->last_pong_recv, NULL, &c->last_pong_recv_usec);
+ create_timestamp(&c->last_pong_send, NULL, &c->last_pong_send_usec);
+ c->latency_usec = 0.0;
+ sodium_mlock(c, sizeof(*c));
+
+ return c;
+}
+
+struct connection * new_connection_from_client(struct longterm_keypair const * const my_keypair)
+{
+ struct connection * c = new_connection(my_keypair);
+
+ if (c == NULL) {
+ return NULL;
+ }
+ c->is_server_side = 1;
+
+ return c;
+}
+
+struct connection * new_connection_to_server(struct longterm_keypair const * const my_keypair)
+{
+ struct connection * c = new_connection(my_keypair);
+
+ if (c == NULL) {
+ return NULL;
+ }
+ c->is_server_side = 0;
+
+ return c;
+}
diff --git a/protocol.h b/protocol.h
new file mode 100644
index 0000000..1232fbb
--- /dev/null
+++ b/protocol.h
@@ -0,0 +1,218 @@
+#ifndef PROTOCOL_H
+#define PROTOCOL_H 1
+
+#include <arpa/inet.h>
+#include <sodium.h>
+#include <stdint.h>
+#include <string.h>
+#include <time.h>
+
+#define PROTOCOL_ATTRIBUTES __attribute__((packed))
+#define PROTOCOL_MAGIC 0xBAADC0DE
+#define PROTOCOL_VERSION 0xDEADCAFE
+#define PROTOCOL_TIME_STRLEN 32
+#define WINDOW_SIZE 65535
+#if WINDOW_SIZE > 65535
+#error "Window size is limited by sizeof(header.body_size)"
+#endif
+
+#define CRYPTO_BYTES_PREAUTH crypto_box_SEALBYTES
+#define CRYPTO_BYTES_POSTAUTH crypto_secretstream_xchacha20poly1305_ABYTES
+#define PLAIN_PACKET_HEADER_SIZE ((size_t)sizeof(struct protocol_header))
+#define CRYPT_PACKET_HEADER_SIZE (PLAIN_PACKET_HEADER_SIZE + CRYPTO_BYTES_POSTAUTH)
+
+#define PLAIN_PACKET_BODY_SIZE(protocol_type) ((size_t)(sizeof(protocol_type) - PLAIN_PACKET_HEADER_SIZE))
+#define CRYPT_PACKET_BODY_SIZE(protocol_type) ((size_t)(PLAIN_PACKET_BODY_SIZE(protocol_type) + CRYPTO_BYTES_POSTAUTH))
+
+#define PLAIN_PACKET_SIZE_TOTAL(protocol_type) \
+ ((size_t)(PLAIN_PACKET_HEADER_SIZE + PLAIN_PACKET_BODY_SIZE(protocol_type)))
+#define CRYPT_PACKET_SIZE_TOTAL(protocol_type) \
+ ((size_t)(CRYPT_PACKET_HEADER_SIZE + CRYPT_PACKET_BODY_SIZE(protocol_type)))
+
+#define CRYPT_PACKET_SIZE_CLIENT_AUTH \
+ ((size_t)(CRYPTO_BYTES_PREAUTH + PLAIN_PACKET_SIZE_TOTAL(struct protocol_client_auth)))
+#define CRYPT_PACKET_SIZE_SERVER_HELO \
+ ((size_t)(CRYPTO_BYTES_PREAUTH + PLAIN_PACKET_SIZE_TOTAL(struct protocol_server_helo)))
+/* special-case: CRYPT_PACKET_SIZE_DATA is a dynamic sized packet */
+#define CRYPT_PACKET_SIZE_DATA CRYPT_PACKET_SIZE_TOTAL(struct protocol_data)
+#define CRYPT_PACKET_SIZE_PING CRYPT_PACKET_SIZE_TOTAL(struct protocol_ping)
+#define CRYPT_PACKET_SIZE_PONG CRYPT_PACKET_SIZE_TOTAL(struct protocol_pong)
+
+enum header_types {
+ TYPE_INVALID = 0,
+
+ TYPE_CLIENT_AUTH,
+ TYPE_SERVER_HELO,
+ TYPE_DATA,
+ TYPE_PING,
+ TYPE_PONG,
+
+ TYPE_COUNT
+};
+
+struct protocol_header {
+ uint32_t magic;
+ uint16_t pdu_type;
+ uint16_t body_size;
+} PROTOCOL_ATTRIBUTES;
+
+struct protocol_client_auth {
+ struct protocol_header header;
+ uint32_t protocol_version;
+ uint8_t nonce[crypto_box_NONCEBYTES];
+ unsigned char server_rx_header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
+
+ /*
+ * REMEMBER that sending the public key alone without any shared secret (e.g. user/pass)
+ * makes your application vulnerable to Man-In-The-Middle if an attacker knows the server's public key.
+ * However the auth packet must be encrypted using the servers public key to
+ * prevent tampering of the client publickey and of course a login and passphrase
+ * should never be sent in plaintext over an insecure network.
+ */
+ uint8_t client_publickey[crypto_kx_PUBLICKEYBYTES];
+ char login[128];
+ char passphrase[128]; /* passphase is not hashed, so authentication APIs like PAM can still used */
+} PROTOCOL_ATTRIBUTES;
+
+struct protocol_server_helo {
+ struct protocol_header header;
+ uint8_t nonce_increment[crypto_box_NONCEBYTES];
+ unsigned char client_rx_header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
+
+ char server_message[128];
+} PROTOCOL_ATTRIBUTES;
+
+struct protocol_data {
+ struct protocol_header header;
+ /* pointer to the dynamic sized packet payload with size header.body_size */
+ uint8_t payload[0];
+} PROTOCOL_ATTRIBUTES;
+
+struct protocol_ping {
+ struct protocol_header header;
+ char timestamp[PROTOCOL_TIME_STRLEN];
+ uint64_t timestamp_usec;
+} PROTOCOL_ATTRIBUTES;
+
+struct protocol_pong {
+ struct protocol_header header;
+ char timestamp[PROTOCOL_TIME_STRLEN];
+ uint64_t timestamp_usec;
+} PROTOCOL_ATTRIBUTES;
+
+enum state { CONNECTION_INVALID = 0, CONNECTION_NEW, CONNECTION_AUTH_SUCCESS };
+
+struct longterm_keypair {
+ uint8_t publickey[crypto_kx_PUBLICKEYBYTES];
+ uint8_t secretkey[crypto_kx_SECRETKEYBYTES];
+};
+
+struct session_keys {
+ uint8_t rx[crypto_kx_SESSIONKEYBYTES]; /* key required to read data from remote `pull' */
+ uint8_t tx[crypto_kx_SESSIONKEYBYTES]; /* key required to send data to remote `push' */
+};
+
+struct connection {
+ enum state state;
+ int is_server_side;
+ size_t awaiting_pong;
+ uint32_t used_protocol_version;
+ /* header received and decrypted, but not yet enough data for body received */
+ int partial_packet_received;
+ /* decrypted header form a partial received PDU */
+ struct protocol_header partial_packet_header;
+
+ /* state required when reading data from remote aka `pull' */
+ crypto_secretstream_xchacha20poly1305_state crypto_rx_state;
+ /* state required when sending data to remote aka `push' */
+ crypto_secretstream_xchacha20poly1305_state crypto_tx_state;
+
+ /* nonce must be incremented before sending or comparing a remote received one */
+ uint8_t last_nonce[crypto_box_NONCEBYTES];
+
+ struct tm last_ping_recv;
+ struct tm last_ping_send;
+ struct tm last_pong_recv;
+ struct tm last_pong_send;
+
+ suseconds_t last_ping_recv_usec;
+ suseconds_t last_ping_send_usec;
+ suseconds_t last_pong_recv_usec;
+ suseconds_t last_pong_send_usec;
+
+ double latency_usec;
+
+ /* generated symmetric session keys used by server and client */
+ struct session_keys * session_keys;
+
+ /* used by server and client to store the respective peer public key */
+ uint8_t peer_publickey[crypto_kx_PUBLICKEYBYTES];
+ struct longterm_keypair const * my_keypair;
+
+ /* reserved for the underlying network io system e.g. libevent */
+ void * user_data;
+};
+
+enum recv_return {
+ RECV_SUCCESS,
+ RECV_FATAL,
+ RECV_FATAL_UNAUTH,
+ RECV_FATAL_CRYPTO_ERROR,
+ RECV_CORRUPT_PACKET,
+ RECV_BUFFER_NEED_MORE_DATA,
+ RECV_CALLBACK_NOT_IMPLEMENTED
+};
+
+/*****************************
+ * PDU receive functionality *
+ *****************************/
+
+enum recv_return process_received(struct connection * const state,
+ uint8_t const * const buffer,
+ size_t * const buffer_size);
+
+/* The following functions have to be implemented in your application e.g. client/server. */
+
+extern enum recv_return protocol_request_client_auth(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed);
+extern enum recv_return protocol_request_server_helo(struct connection * const,
+ struct protocol_header const * const buffer,
+ size_t * const processed);
+extern enum recv_return protocol_request_data(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed);
+extern enum recv_return protocol_request_ping(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed);
+extern enum recv_return protocol_request_pong(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed);
+
+/*******************************
+ * PDU send functionality *
+ *******************************/
+
+void protocol_response_client_auth(unsigned char out[CRYPT_PACKET_SIZE_CLIENT_AUTH],
+ struct connection * const state,
+ const char * const user,
+ const char * const pass);
+void protocol_response_server_helo(unsigned char out[CRYPT_PACKET_SIZE_SERVER_HELO],
+ struct connection * const state,
+ const char * const welcome_message);
+void protocol_response_data(uint8_t * const out,
+ size_t out_size,
+ struct connection * const state,
+ uint8_t const * const payload,
+ size_t payload_size);
+void protocol_response_ping(unsigned char out[CRYPT_PACKET_SIZE_PING], struct connection * const state);
+void protocol_response_pong(unsigned char out[CRYPT_PACKET_SIZE_PONG], struct connection * const state);
+
+/**********************************
+ * connection state functionality *
+ **********************************/
+
+struct connection * new_connection_from_client(struct longterm_keypair const * const my_keypair);
+struct connection * new_connection_to_server(struct longterm_keypair const * const my_keypair);
+
+#endif
diff --git a/server.c b/server.c
new file mode 100644
index 0000000..d679c87
--- /dev/null
+++ b/server.c
@@ -0,0 +1,368 @@
+#include <arpa/inet.h>
+#include <errno.h>
+#include <event2/buffer.h>
+#include <event2/bufferevent.h>
+#include <event2/event.h>
+#include <event2/listener.h>
+#include <event2/util.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "common-event2.h"
+#include "common-sodium.h"
+#include "logging.h"
+#include "protocol.h"
+#include "utils.h"
+
+static struct cmd_options opts = {.key_string = NULL, .key_length = 0, .host = NULL, .port = 0, .filepath = NULL};
+static int data_fd = -1;
+
+static void recv_data(uint8_t const * const buffer, size_t size)
+{
+ ssize_t bytes_written;
+
+ if (data_fd >= 0) {
+ bytes_written = write(data_fd, buffer, size);
+ if (bytes_written < 0) {
+ LOG(WARNING, "Closing file descriptor %d aka %s: %s", data_fd, opts.filepath, strerror(errno));
+ close(data_fd);
+ data_fd = -1;
+ } else {
+ LOG(NOTICE, "Recv DATA: %zd", bytes_written);
+ }
+ }
+}
+
+enum recv_return protocol_request_client_auth(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_client_auth const * const auth_pkt = (struct protocol_client_auth *)buffer;
+
+ (void)processed;
+ LOG(NOTICE, "Client AUTH with protocol version 0x%X", state->used_protocol_version);
+
+ /* user/pass authentication part - exemplary */
+ if (strncmp(auth_pkt->login, "username", sizeof(auth_pkt->login)) == 0 &&
+ strncmp(auth_pkt->passphrase, "passphrase", sizeof(auth_pkt->passphrase)) == 0) {
+
+ LOG(NOTICE,
+ "Username '%.*s' with passphrase '%.*s' logged in",
+ sizeof(auth_pkt->login),
+ auth_pkt->login,
+ sizeof(auth_pkt->passphrase),
+ auth_pkt->passphrase);
+ } else {
+ LOG(ERROR, "Authentication failed, username/passphrase mismatch");
+ return RECV_FATAL_UNAUTH;
+ }
+
+ log_bin2hex_sodium("Client AUTH with PublicKey", auth_pkt->client_publickey, sizeof(auth_pkt->client_publickey));
+
+ if (generate_session_keypair_sodium(state) != 0) {
+ LOG(ERROR, "Client session keypair generation failed");
+ return RECV_FATAL;
+ }
+ crypto_secretstream_xchacha20poly1305_init_pull(&state->crypto_rx_state,
+ auth_pkt->server_rx_header,
+ state->session_keys->rx);
+
+ if (ev_protocol_server_helo(state, "Welcome.") != 0) {
+ LOG(ERROR, "Server AUTH response failed");
+ return RECV_FATAL;
+ }
+ if (ev_setup_generic_timer((struct ev_user_data *)state->user_data, PING_INTERVAL) != 0) {
+ LOG(ERROR, "Timer init failed");
+ return RECV_FATAL;
+ }
+
+ state->state = CONNECTION_AUTH_SUCCESS;
+ return RECV_SUCCESS;
+}
+
+enum recv_return protocol_request_server_helo(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ (void)state;
+ (void)buffer;
+ (void)processed;
+ return RECV_CALLBACK_NOT_IMPLEMENTED;
+}
+
+enum recv_return protocol_request_data(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_data const * const data_pkt = (struct protocol_data *)buffer;
+ char response[32];
+
+ (void)state;
+ (void)processed;
+ LOG(NOTICE, "Received DATA with size: %u", data_pkt->header.body_size);
+ log_bin2hex_sodium("DATA", data_pkt->payload, data_pkt->header.body_size);
+ recv_data(data_pkt->payload, data_pkt->header.body_size);
+ snprintf(response, sizeof(response), "DATA OK: RECEIVED %u BYTES", data_pkt->header.body_size);
+ if (ev_protocol_data(state, (uint8_t *)response, sizeof(response)) != 0) {
+ return RECV_FATAL;
+ }
+ return RECV_SUCCESS;
+}
+
+enum recv_return protocol_request_ping(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_ping const * const ping_pkt = (struct protocol_ping *)buffer;
+
+ (void)processed;
+ LOG(NOTICE,
+ "Received PING with timestamp: %.*s / %lluus",
+ sizeof(ping_pkt->timestamp),
+ ping_pkt->timestamp,
+ state->last_ping_recv_usec);
+ if (state->latency_usec > 0.0) {
+ LOG(NOTICE, "PING-PONG latency: %.02lfms", state->latency_usec / 1000.0);
+ }
+
+ if (ev_protocol_pong(state) != 0) {
+ return RECV_FATAL;
+ } else {
+ return RECV_SUCCESS;
+ }
+}
+
+enum recv_return protocol_request_pong(struct connection * const state,
+ struct protocol_header const * const buffer,
+ size_t * const processed)
+{
+ struct protocol_pong const * const pong_pkt = (struct protocol_pong *)buffer;
+
+ (void)processed;
+ LOG(NOTICE,
+ "Received PONG with timestamp: %.*s / %lluus / %zu outstanding PONG's",
+ sizeof(pong_pkt->timestamp),
+ pong_pkt->timestamp,
+ state->last_pong_recv_usec,
+ state->awaiting_pong);
+
+ return RECV_SUCCESS;
+}
+
+void on_disconnect(struct connection * const state)
+{
+ (void)state;
+}
+
+static void event_cb(struct bufferevent * bev, short events, void * con)
+{
+ struct connection * const c = (struct connection *)con;
+ char events_string[64] = {0};
+
+ ev_events_to_string(events, events_string, sizeof(events_string));
+ LOG(LP_DEBUG, "Event(s): 0x%02X (%s)", events, events_string);
+
+ if (events & BEV_EVENT_ERROR) {
+ LOG(ERROR, "Error from bufferevent: %s", strerror(errno));
+ return;
+ }
+ if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
+ LOG(NOTICE, "Client closed connection");
+ ev_disconnect(c);
+ return;
+ }
+ if (events & EV_TIMEOUT) {
+ LOG(NOTICE, "Timeout");
+ bufferevent_enable(bev, EV_READ | EV_WRITE);
+ ev_disconnect(c);
+ return;
+ }
+}
+
+static void accept_conn_cb(
+ struct evconnlistener * listener, evutil_socket_t fd, struct sockaddr * address, int socklen, void * user_data)
+{
+ struct connection * c;
+ struct event_base * base;
+ struct bufferevent * bev;
+ char ip_str[INET6_ADDRSTRLEN + 1];
+ struct longterm_keypair const * my_keypair;
+
+ (void)address;
+ (void)socklen;
+
+ if (user_data == NULL) {
+ return;
+ }
+ my_keypair = (struct longterm_keypair *)user_data;
+
+ base = evconnlistener_get_base(listener);
+ bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_UNLOCK_CALLBACKS);
+
+ if (bev == NULL) {
+ return;
+ }
+
+ c = new_connection_from_client(my_keypair);
+ if (c == NULL) {
+ bufferevent_free(bev);
+ return;
+ }
+
+ if (ev_setup_user_data(bev, c) != 0 ||
+ ev_setup_generic_timer((struct ev_user_data *)c->user_data, AUTHENTICATION_TIMEOUT) != 0) {
+ ev_disconnect(c);
+ return;
+ }
+
+ bufferevent_setcb(bev, ev_read_cb, ev_write_cb, event_cb, c);
+ if (bufferevent_enable(bev, EV_READ | EV_WRITE) != 0) {
+ ev_disconnect(c);
+ return;
+ }
+ ev_set_io_timeouts(bev);
+
+ if (inet_ntop(AF_INET, &((struct sockaddr_in *)address)->sin_addr, ip_str, INET_ADDRSTRLEN) == NULL &&
+ inet_ntop(AF_INET6, &((struct sockaddr_in6 *)address)->sin6_addr, ip_str, INET6_ADDRSTRLEN) == NULL) {
+ ip_str[0] = '\0';
+ }
+ LOG(NOTICE, "Accepted %s:%u", ip_str, ntohs(((struct sockaddr_in6 *)address)->sin6_port));
+}
+
+static void accept_error_cb(struct evconnlistener * listener, void * ctx)
+{
+ struct event_base * base = evconnlistener_get_base(listener);
+ int err = EVUTIL_SOCKET_ERROR();
+
+ (void)ctx;
+ LOG(ERROR, "Got an error %d (%s) on the listener.", err, evutil_socket_error_to_string(err));
+ event_base_loopexit(base, NULL);
+}
+
+static void cleanup(struct event_base ** const ev_base,
+ struct evconnlistener ** const ev_listener,
+ struct event ** const ev_sig,
+ struct longterm_keypair ** const my_keypair)
+{
+ if (*my_keypair != NULL) {
+ free(*my_keypair);
+ }
+ if (*ev_sig != NULL) {
+ event_free(*ev_sig);
+ }
+ if (*ev_listener != NULL) {
+ evconnlistener_free(*ev_listener);
+ }
+ if (*ev_base != NULL) {
+ event_base_free(*ev_base);
+ }
+ *my_keypair = NULL;
+ *ev_sig = NULL;
+ *ev_listener = NULL;
+ *ev_base = NULL;
+}
+
+__attribute__((noreturn)) static void cleanup_and_exit(struct event_base ** const ev_base,
+ struct evconnlistener ** const ev_listener,
+ struct event ** const ev_sigint,
+ struct longterm_keypair ** const my_keypair,
+ int exit_code)
+{
+ LOG(LP_DEBUG, "Cleanup and exit with exit code: %d", exit_code);
+ cleanup(ev_base, ev_listener, ev_sigint, my_keypair);
+ exit(exit_code);
+}
+
+int main(int argc, char ** argv)
+{
+ struct longterm_keypair * my_keypair = NULL;
+ struct event_base * ev_base = NULL;
+ struct event * ev_sig = NULL;
+ struct evconnlistener * ev_listener = NULL;
+ struct sockaddr_in sin;
+
+ char ip_str[INET6_ADDRSTRLEN + 1];
+
+ parse_cmdline(&opts, argc, argv);
+ if (opts.key_string != NULL && opts.key_length != crypto_kx_PUBLICKEYBYTES * 2 /* hex string */) {
+ LOG(ERROR, "Invalid server private key length: %zu", opts.key_length);
+ return 1;
+ }
+ if (opts.filepath != NULL) {
+ data_fd = open(opts.filepath, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+ if (data_fd < 0) {
+ LOG(ERROR, "File '%s' open() error: %s", opts.filepath, strerror(errno));
+ return 1;
+ }
+ }
+ if (opts.port <= 0 || opts.port > 65535) {
+ LOG(ERROR, "Invalid port: %d", opts.port);
+ return 2;
+ }
+
+ srandom(time(NULL));
+
+ if (sodium_init() != 0) {
+ LOG(ERROR, "Sodium init failed");
+ return 2;
+ }
+
+ if (init_sockaddr_inet(&sin, opts.host, opts.port, ip_str) != 0) {
+ return 3;
+ }
+ LOG(NOTICE, "Listen on %s:%u", ip_str, opts.port);
+
+ if (opts.key_string != NULL) {
+ my_keypair = generate_keypair_from_secretkey_hexstr_sodium(opts.key_string, opts.key_length);
+ } else {
+ LOG(NOTICE, "No private key set via command line, generating a new keypair..");
+ my_keypair = generate_keypair_sodium();
+ }
+ if (my_keypair == NULL) {
+ LOG(ERROR, "Sodium keypair generation failed");
+ cleanup_and_exit(&ev_base, &ev_listener, &ev_sig, &my_keypair, 4);
+ }
+ if (opts.key_string == NULL) {
+ log_bin2hex_sodium("Server PrivateKey", my_keypair->secretkey, sizeof(my_keypair->secretkey));
+ }
+ log_bin2hex_sodium("Server PublicKey", my_keypair->publickey, sizeof(my_keypair->publickey));
+
+ ev_base = event_base_new();
+ if (ev_base == NULL) {
+ LOG(ERROR, "Couldn't open event base");
+ cleanup_and_exit(&ev_base, &ev_listener, &ev_sig, &my_keypair, 5);
+ }
+
+ ev_sig = evsignal_new(ev_base, SIGINT, ev_sighandler, event_self_cbarg());
+ if (ev_sig == NULL) {
+ cleanup_and_exit(&ev_base, &ev_listener, &ev_sig, &my_keypair, 6);
+ }
+ if (event_add(ev_sig, NULL) != 0) {
+ cleanup_and_exit(&ev_base, &ev_listener, &ev_sig, &my_keypair, 7);
+ }
+
+ ev_listener = evconnlistener_new_bind(ev_base,
+ accept_conn_cb,
+ my_keypair,
+ LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
+ -1,
+ (struct sockaddr *)&sin,
+ sizeof(sin));
+ if (ev_listener == NULL) {
+ LOG(ERROR, "Couldn't create listener: %s", strerror(errno));
+ cleanup_and_exit(&ev_base, &ev_listener, &ev_sig, &my_keypair, 8);
+ }
+ evconnlistener_set_error_cb(ev_listener, accept_error_cb);
+ event_base_dispatch(ev_base);
+
+ LOG(NOTICE, "shutdown");
+ cleanup_and_exit(&ev_base, &ev_listener, &ev_sig, &my_keypair, 0);
+}
diff --git a/utils.h b/utils.h
new file mode 100644
index 0000000..b4ed2a2
--- /dev/null
+++ b/utils.h
@@ -0,0 +1,70 @@
+#ifndef UTILS_H
+#define UTILS_H 1
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+struct cmd_options {
+ /* server: private key
+ * client: server public key
+ */
+ char * key_string;
+ size_t key_length;
+ /* server: listen host
+ * client: remote host
+ */
+ char * host;
+ /* server: listen port
+ * client: remote port
+ */
+ int port;
+ /* server: path to write to, received from client via PDU-type DATA
+ * client: path to read from, send it via PDU-type DATA
+ */
+ char * filepath;
+};
+
+__attribute__((noreturn)) static inline void usage(const char * const arg0)
+{
+ fprintf(stderr, "usage: %s -k [SODIUM-KEY] -h [HOST] -p [PORT] -f [FILE]\n", arg0);
+ exit(EXIT_FAILURE);
+}
+
+static inline void parse_cmdline(struct cmd_options * const opts, int argc, char ** const argv)
+{
+ int opt;
+
+ while ((opt = getopt(argc, argv, "k:h:p:f:h")) != -1) {
+ switch (opt) {
+ case 'k':
+ opts->key_string = strdup(optarg);
+ memset(optarg, '*', strlen(optarg));
+ break;
+ case 'h':
+ opts->host = strdup(optarg);
+ break;
+ case 'p':
+ opts->port = atoi(optarg); /* meh, strtol is king */
+ break;
+ case 'f':
+ opts->filepath = strdup(optarg);
+ break;
+ default:
+ usage(argv[0]);
+ }
+ }
+
+ if (opts->host == NULL) {
+ opts->host = strdup("127.0.0.1");
+ }
+ if (opts->port == 0) {
+ opts->port = 5555;
+ }
+ if (opts->key_string != NULL) {
+ opts->key_length = strlen(opts->key_string);
+ }
+}
+
+#endif