diff options
author | Toni Uhlig <matzeton@googlemail.com> | 2020-05-22 13:43:46 +0200 |
---|---|---|
committer | Toni Uhlig <matzeton@googlemail.com> | 2020-05-22 14:48:29 +0200 |
commit | c394c09330760985d282cb866a06dea6294012aa (patch) | |
tree | 5a120d309ef25552b719844474993184a8707608 |
first public release
Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
-rw-r--r-- | .clang-format | 77 | ||||
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | Makefile | 113 | ||||
-rw-r--r-- | README.md | 39 | ||||
-rw-r--r-- | client.c | 325 | ||||
-rw-r--r-- | common-event2.c | 357 | ||||
-rw-r--r-- | common-event2.h | 63 | ||||
-rw-r--r-- | common-sodium.c | 106 | ||||
-rw-r--r-- | common-sodium.h | 24 | ||||
-rw-r--r-- | logging.c | 59 | ||||
-rw-r--r-- | logging.h | 10 | ||||
-rw-r--r-- | protocol.c | 599 | ||||
-rw-r--r-- | protocol.h | 218 | ||||
-rw-r--r-- | server.c | 368 | ||||
-rw-r--r-- | utils.h | 70 |
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); +} @@ -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 |