diff --git a/Makefile b/Makefile index 9eb3e12..2c02aac 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,11 @@ ifeq ($(WITH_LUA),yes) OTR_OBJS += hooks.o endif +ifeq ($(WITH_ENCRYPT),yes) + CFLAGS += -DWITH_ENCRYPT=1 $(SODIUM_CFLAGS) + LIBS += $(SODIUM_LIBS) +endif + ifeq ($(WITH_KILL),yes) CFLAGS += -DWITH_KILL=1 endif diff --git a/README.md b/README.md index d1e8c62..137c579 100644 --- a/README.md +++ b/README.md @@ -721,6 +721,24 @@ ocat --dump | This named lmdb database is keyed on topic name (`owntracks/jane/phone`). If the topic of an incoming message is found in the database, the `tid` member in the JSON payload is replaced by the string value of this key. +#### `keys` + +If the _recorder_ was built with encryption support (see below), this named database contains the secret decryption keys for users/device pairs. The LMDB key is the username followed by a dash followed by the device name, all lower case. For example, if user Jjolie with device iPhone needs a secret entered, the database key will be `jjolie-iphone`. This can be entered into the database as follows: + +```bash +echo "jjolie-iphone s3cr1t" | ocat --load=keys +``` + +Beware: these secret keys are stored in plain text so the database must be protected! + +## Encryption (*experimental!*) + +If compiled with `WITH_ENCRYPT` support, the recorder will handle messages from OwnTracks devices which support payload encryption. Each user / device requires a secret key which is configured on the device and which must be configured on the Recorder host in order for the Recorder to be able to decrypt the payloads. + +Upon successful decryption, the Recorder processes the original (device-transmitted) JSON and stores the result in plain (i.e. un-encrypted) form in the store. + + + ## Prerequisites for building You need a current version of the Mosquitto library (and you probably require the Mosquitto broker as well for OwnTracks). We strongly recommend installing Mosquitto either from [source](http://mosquitto.org/download/) or from a [binary package](http://mosquitto.org/download/), both of which are provided by the [Mosquitto project](http://mosquitto.org/). In particular, older or LTS OS versions profit from this. diff --git a/config.mk.in b/config.mk.in index 0b6f9d3..fb2b9c7 100644 --- a/config.mk.in +++ b/config.mk.in @@ -17,6 +17,9 @@ WITH_PING ?= yes # Do you want support for removing data via the API? (Dangerous) WITH_KILL ?= no +# Do you want support for payload encryption with libsodium? +WITH_ENCRYPT ?= no + # Do you want R_only support? (Probably not; this is for Hosted) # If you set this to `yes', WITH_LMDB will be set to yes WITH_RONLY ?= no @@ -24,6 +27,7 @@ WITH_RONLY ?= no # Do you require support for OwnTracks Greenwich firmware? WITH_GREENWICH ?= no + # Where should the recorder store its data? This directory must # exist and be writeable by recorder (and readable by ocat) STORAGEDEFAULT = /var/spool/owntracks/recorder/store @@ -63,3 +67,6 @@ MORELIBS = # -lssl LUA_CFLAGS = `pkg-config --cflags lua` LUA_LIBS = `pkg-config --libs lua` + +SODIUM_CFLAGS = `pkg-config --cflags libsodium` +SODIUM_LIBS = `pkg-config --libs libsodium` diff --git a/ocat.c b/ocat.c index b5dc498..92fc3d1 100644 --- a/ocat.c +++ b/ocat.c @@ -97,6 +97,9 @@ void print_versioninfo() #ifdef WITH_HTTP printf("\tWITH_HTTP = yes\n"); #endif +#ifdef WITH_ENCRYPT + printf("\tWITH_ENCRYPT = yes\n"); +#endif #ifdef WITH_PING printf("\tWITH_PING = yes\n"); #endif diff --git a/recorder.c b/recorder.c index f0014c8..088488d 100644 --- a/recorder.c +++ b/recorder.c @@ -50,6 +50,9 @@ #ifdef WITH_LUA # include "hooks.h" #endif +#if WITH_ENCRYPT +# include +#endif #define SSL_VERIFY_PEER (1) @@ -549,6 +552,81 @@ void store_gwvalue(char *username, char *device, time_t tst, char *key, char *va } #endif /* GREENWICH */ +#if WITH_ENCRYPT +/* + * Create a new mosquitto message structure, decrypt and populate new. + * p64 contains the base64-encoded payload from the device. `username' + * and `device' are needed to obtain the decryption key for this object. + */ + +struct mosquitto_message *decrypt(struct udata *ud, const struct mosquitto_message *m, char *p64, char *username, char *device) +{ + struct mosquitto_message *msg; + unsigned char key[crypto_secretbox_KEYBYTES]; + unsigned char *ciphertext, *cleartext; + unsigned long ciphertext_len; + int n, klen; + UT_string *userdev; + + + utstring_new(userdev); + utstring_printf(userdev, "%s-%s", username, device); + + memset(key, 0, sizeof(key)); + klen = gcache_get(ud->keydb, (char *)UB(userdev), (char *)key, sizeof(key)); + if (klen < 1) { + olog(LOG_ERR, "no decryption key for %s in %s", UB(userdev), m->topic); + return (NULL); + } + + if ((msg = malloc(sizeof(struct mosquitto_message))) == NULL) { + return (NULL); + } + + n = strlen(p64); /* This is more than enough */ + + msg->mid = m->mid; + msg->topic = m->topic; + msg->qos = m->qos; + msg->retain = m->retain; + + if ((ciphertext = malloc(n)) == NULL) { + free(msg); + return (NULL); + } + ciphertext_len = base64_decode(p64, ciphertext); + + fprintf(stderr, "START DECRYPT. clen==%lu\n", ciphertext_len); + + if ((cleartext = calloc(n, sizeof(unsigned char))) == NULL) { + free(ciphertext); + free(msg); + return (NULL); + } + + if (crypto_secretbox_open_easy(cleartext, // message + ciphertext + crypto_secretbox_NONCEBYTES, // authtag + encrypted + ciphertext_len - crypto_secretbox_NONCEBYTES, // len (auth+encr) + ciphertext, // nonce + key) != 0) + { + olog(LOG_ERR, "payload of %s cannot be decrypted; forged?", m->topic); + free(ciphertext); + free(cleartext); + free(msg); + return (NULL); + } + + printf("DECRYPTED: %s\n", (char *)cleartext); + free(ciphertext); + + msg->payload = (void *)cleartext; + msg->payloadlen = strlen((char *)cleartext); + + return (msg); +} +#endif /* ENCRYPT */ + void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *m) { JsonNode *json, *j, *geo = NULL; @@ -565,6 +643,9 @@ void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_m int pingping = FALSE, skipslash = 0; int r_ok = TRUE; /* True if recording enabled for a publish */ payload_type _type; +#ifdef WITH_ENCRYPT + struct mosquitto_message *new_m; +#endif /* * mosquitto_message-> @@ -759,6 +840,9 @@ void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_m else if (!strcmp(j->string_, "waypoint")) _type = T_WAYPOINT; else if (!strcmp(j->string_, "waypoints")) _type = T_WAYPOINTS; else if (!strcmp(j->string_, "dump")) _type = T_CONFIG; +#if WITH_ENCRYPT + else if (!strcmp(j->string_, "encrypted")) _type = T_ENCRYPTED; +#endif /* WITH_ENCRYPT */ } } @@ -799,6 +883,30 @@ void on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_m case T_TRANSITION: case T_LOCATION: break; +#if WITH_ENCRYPT + case T_ENCRYPTED: + /* + * Obtain the `data' element from JSON, and try and decrypt + * that. If successful, we get a new mosquitto_message with + * the decrypted message as payload, and invoke this function + * again to do the heavy lifting. + */ + + if ((j = json_find_member(json, "data")) != NULL) { + if (j->tag == JSON_STRING) { + new_m = decrypt(ud, m, j->string_, UB(username), UB(device)); + if (new_m != NULL) { + on_message(mosq, userdata, new_m); + free(new_m->payload); + free(new_m); + } + return; + } + } + olog(LOG_ERR, "no `data' in encrypted %s", m->topic); + return; + break; +#endif /* WITH_ENCRYPT */ default: if (r_ok) { putrec(ud, now, reltopic, username, device, bindump(m->payload, m->payloadlen)); @@ -1399,6 +1507,13 @@ int main(int argc, char **argv) } gcache_close(gt); #endif /* !RONLY */ +#ifdef WITH_ENCRYPT + if ((gt = gcache_open(path, "keys", FALSE)) == NULL) { + fprintf(stderr, "Cannot lmdb-open `keys'\n"); + exit(2); + } + gcache_close(gt); +#endif /* !ENCRYPT */ #endif exit(0); } @@ -1488,6 +1603,9 @@ int main(int argc, char **argv) # ifdef WITH_RONLY ud->ronlydb = gcache_open(err, "ronlydb", FALSE); # endif +# ifdef WITH_ENCRYPT + ud->keydb = gcache_open(err, "keys", TRUE); +# endif #endif #if WITH_LUA && WITH_LMDB @@ -1503,6 +1621,12 @@ int main(int argc, char **argv) } #endif +#if WITH_ENCRYPT + if (sodium_init() == -1) { + olog(LOG_ERR, "cannot initialize libsodium"); + } +#endif + mosquitto_lib_init(); diff --git a/storage.h b/storage.h index a2ca2f7..2b696e4 100644 --- a/storage.h +++ b/storage.h @@ -33,6 +33,9 @@ typedef enum { T_TRANSITION, T_WAYPOINT, T_WAYPOINTS, +#if WITH_ENCRYPT + T_ENCRYPTED, +#endif } payload_type; JsonNode *lister(char *username, char *device, time_t s_lo, time_t s_hi, int reverse); diff --git a/udata.h b/udata.h index 665ef9d..0ee2b13 100644 --- a/udata.h +++ b/udata.h @@ -35,6 +35,9 @@ struct udata { # ifdef WITH_LMDB struct gcache *luadb; /* lmdb named database 'luadb' */ # endif +# ifdef WITH_ENCRYPT + struct gcache *keydb; /* encryption keys */ +# endif #endif char *label; /* Server label */ char *geokey; /* Google reverse-geo API key */