From dde3e0ccb2ad027a67eeb101ac5c6d9b546f5dac Mon Sep 17 00:00:00 2001 From: Bram Matthys Date: Mon, 3 May 2021 15:07:10 +0200 Subject: [PATCH] Add unrealdb and secrets API. Documentation and more information will follow in later commits. --- include/h.h | 51 +++ include/struct.h | 86 ++++ include/unrealircd.h | 2 +- src/Makefile.in | 5 +- src/api-event.c | 2 + src/auth.c | 1 - src/conf.c | 234 +++++++++- src/ircd.c | 11 +- src/misc.c | 65 +++ src/support.c | 28 ++ src/unrealdb.c | 1040 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1517 insertions(+), 8 deletions(-) create mode 100644 src/unrealdb.c diff --git a/include/h.h b/include/h.h index 91464edec..06ad7d339 100644 --- a/include/h.h +++ b/include/h.h @@ -545,9 +545,31 @@ extern void *safe_alloc(size_t size); */ #define raw_strldup(str, max) our_strldup(str, max) +extern void *safe_alloc_sensitive(size_t size); + +/** Free previously allocate memory pointer - this is the sensitive version which + * may ONLY be called on allocations returned by safe_alloc_sensitive() / safe_strdup_sensitive(). + * This will set the memory to all zeroes before actually deallocating. + * It also sets the pointer to NULL, since that would otherwise be common to forget. + * @note If you call this function on normally allocated memory (non-sensitive) then we will crash. + */ +#define safe_free_sensitive(x) do { if (x) sodium_free(x); x = NULL; } while(0) + +/** Free previous memory (if any) and then save a duplicate of the specified string - + * This is the 'sensitive' version which should only be used for HIGHLY sensitive data, + * as it wastes about 8000 bytes even if you only duplicate a string of 32 bytes (this is by design). + * @param dst The current pointer and the pointer where a new copy of the string will be stored. + * @param str The string you want to copy + */ +#define safe_strdup_sensitive(dst,str) do { if (dst) sodium_free(dst); if (!(str)) dst = NULL; else dst = our_strdup_sensitive(str); } while(0) + +/** Safely destroy a string in memory (but do not free!) */ +#define destroy_string(str) sodium_memzero(str, strlen(str)) + /** @} */ extern char *our_strdup(const char *str); extern char *our_strldup(const char *str, size_t max); +extern char *our_strdup_sensitive(const char *str); extern long config_checkval(char *value, unsigned short flags); extern void config_status(FORMAT_STRING(const char *format), ...) __attribute__((format(printf,1,2))); @@ -1007,3 +1029,32 @@ extern NameValuePrioList *find_nvplist(NameValuePrioList *list, char *name); extern void free_nvplist(NameValuePrioList *lst); extern char *get_connect_extinfo(Client *client); extern char *unreal_strftime(char *str); +/* src/unrealdb.c start */ +extern UnrealDB *unrealdb_open(const char *filename, UnrealDBMode mode, char *secret_block); +extern int unrealdb_close(UnrealDB *c); +extern char *unrealdb_test_db(const char *filename, char *secret_block); +extern int unrealdb_write_int64(UnrealDB *c, uint64_t t); +extern int unrealdb_write_int32(UnrealDB *c, uint32_t t); +extern int unrealdb_write_int16(UnrealDB *c, uint16_t t); +extern int unrealdb_write_str(UnrealDB *c, char *x); +extern int unrealdb_write_data(UnrealDB *c, void *buf, int len); +extern int unrealdb_read_int64(UnrealDB *c, uint64_t *t); +extern int unrealdb_read_int32(UnrealDB *c, uint32_t *t); +extern int unrealdb_read_int16(UnrealDB *c, uint16_t *t); +extern int unrealdb_read_str(UnrealDB *c, char **x); +extern int unrealdb_read_data(UnrealDB *c, void *buf, int len); +extern char *unrealdb_test_secret(char *name); +extern UnrealDBConfig *unrealdb_copy_config(UnrealDBConfig *src); +extern UnrealDBConfig *unrealdb_get_config(UnrealDB *db); +extern void unrealdb_free_config(UnrealDBConfig *c); +extern UnrealDBError unrealdb_get_error_code(void); +extern char *unrealdb_get_error_string(void); +/* src/unrealdb.c end */ +/* secret { } related stuff */ +extern Secret *find_secret(char *secret_name); +extern void free_secret_cache(SecretCache *c); +extern void free_secret(Secret *s); +extern Secret *secrets; +/* end */ +extern int check_password_strength(char *pass, int min_length, int strict, char **err); +extern int valid_secret_password(char *pass, char **err); diff --git a/include/struct.h b/include/struct.h index 21c4c006b..59e95280e 100644 --- a/include/struct.h +++ b/include/struct.h @@ -109,6 +109,7 @@ typedef struct ConfigItem_blacklist_module ConfigItem_blacklist_module; typedef struct ConfigItem_help ConfigItem_help; typedef struct ConfigItem_offchans ConfigItem_offchans; typedef struct SecurityGroup SecurityGroup; +typedef struct Secret Secret; typedef struct ListStruct ListStruct; typedef struct ListStructPrio ListStructPrio; @@ -862,6 +863,91 @@ typedef void (*CmdFunc)(Client *client, MessageTag *mtags, int parc, char *parv[ typedef void (*AliasCmdFunc)(Client *client, MessageTag *mtags, int parc, char *parv[], char *cmd); typedef void (*OverrideCmdFunc)(CommandOverride *ovr, Client *client, MessageTag *mtags, int parc, char *parv[]); +#include + +/* This is the 'chunk size', the size of encryption blocks. + * We choose 4K here since that is a decent amount as of 2021 and + * more would not benefit performance anyway. + * Note that you cannot change this value easily afterwards + * (you cannot read files with a different chunk size). + */ +#define UNREALDB_CRYPT_FILE_CHUNK_SIZE 4096 + +/** The salt length. Don't change. */ +#define UNREALDB_SALT_LEN 16 + +/** Database modes of operation (read or write) */ +typedef enum UnrealDBMode { + UNREALDB_MODE_READ = 0, + UNREALDB_MODE_WRITE = 1 +} UnrealDBMode; + +typedef enum UnrealDBCipher { + UNREALDB_CIPHER_XCHACHA20 = 0x0001 +} UnrealDBCipher; + +typedef enum UnrealDBKDF { + UNREALDB_KDF_ARGON2ID = 0x0001 +} UnrealDBKDF; + +/** Database configuration for a particular file */ +typedef struct UnrealDBConfig { + uint16_t kdf; /**< Key derivation function (always 0x01) */ + uint16_t t_cost; /**< Time cost (number of rounds) */ + uint16_t m_cost; /**< Memory cost (in number of bitshifts, eg 15 means 1<<15=32M) */ + uint16_t p_cost; /**< Parallel cost (number of concurrent threads) */ + uint16_t saltlen; /**< Length of the salt (normally UNREALDB_SALT_LEN) */ + char *salt; /**< Salt */ + uint16_t cipher; /**< Encryption cipher (always 0x01) */ + uint16_t keylen; /**< Key length */ + char *key; /**< The key used for encryption/decryption */ +} UnrealDBConfig; + +typedef enum UnrealDBError { + UNREALDB_ERROR_SUCCESS = 0, /**< Success, not an error */ + UNREALDB_ERROR_FILENOTFOUND = 1, /**< File does not exist */ + UNREALDB_ERROR_CRYPTED = 2, /**< File is crypted but no password provided */ + UNREALDB_ERROR_NOTCRYPTED = 3, /**< File is not crypted and a password was provided */ + UNREALDB_ERROR_HEADER = 4, /**< Header is corrupt, invalid or unknown format */ + UNREALDB_ERROR_SECRET = 5, /**< Invalid secret { } block provided - either does not exist or does not meet requirements */ + UNREALDB_ERROR_PASSWORD = 6, /**< Invalid password provided */ + UNREALDB_ERROR_IO = 7, /**< I/O error */ + UNREALDB_ERROR_API = 8, /**< API call violation, eg requesting to write on a file opened for reading */ + UNREALDB_ERROR_INTERNAL = 9, /**< Internal error, eg crypto routine returned something unexpected */ +} UnrealDBError; + +/** Database handle. + * This is returned by unrealdb_open() and used by all other unrealdb_* functions. + */ +typedef struct UnrealDB { + FILE *fd; /**< File descriptor */ + UnrealDBMode mode; /**< UNREALDB_MODE_READ / UNREALDB_MODE_WRITE */ + int crypted; /**< Are we doing any encryption or just plaintext? */ + uint64_t creationtime; /**< When this file was created/updates */ + crypto_secretstream_xchacha20poly1305_state st; /**< Internal state for crypto engine */ + char buf[UNREALDB_CRYPT_FILE_CHUNK_SIZE]; /**< Buffer used for reading/writing */ + int buflen; /**< Length of current data in buffer */ + UnrealDBError error_code; /**< Last error code. Whenever this happens we will set this, never overwrite, and block further I/O */ + char *error_string; /**< Error string upon failure */ + UnrealDBConfig *config; /**< Config */ +} UnrealDB; + +/** Used for speeding up reading/writing of DBs (so we don't have to run argon2 repeatedly) */ +typedef struct SecretCache SecretCache; +struct SecretCache { + SecretCache *prev, *next; + UnrealDBConfig *config; + time_t cache_hit; +}; + +/** Used for storing secret { } blocks */ +struct Secret { + Secret *prev, *next; + char *name; + char *password; + SecretCache *cache; +}; + /* tkl: * TKL_KILL|TKL_GLOBAL = Global K-Line (GLINE) diff --git a/include/unrealircd.h b/include/unrealircd.h index 6c9f47f44..1b7bbbd2c 100644 --- a/include/unrealircd.h +++ b/include/unrealircd.h @@ -35,4 +35,4 @@ #ifdef USE_LIBCURL #include #endif -#include +#include diff --git a/src/Makefile.in b/src/Makefile.in index 29508bcbf..a50e095d8 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -31,7 +31,7 @@ OBJS=dns.o auth.o channel.o crule.o dbuf.o \ api-moddata.o api-extban.o api-isupport.o api-command.o \ api-clicap.o api-messagetag.o api-history-backend.o api-efunctions.o \ api-event.o \ - crypt_blowfish.o updconf.o crashreport.o modulemanager.o \ + crypt_blowfish.o unrealdb.o updconf.o crashreport.o modulemanager.o \ utf8.o \ openssl_hostname_validation.o $(URL) @@ -224,6 +224,9 @@ api-efunctions.o: api-efunctions.c $(INCLUDES) crypt_blowfish.o: crypt_blowfish.c $(INCLUDES) $(CC) $(CFLAGS) $(BINCFLAGS) -c crypt_blowfish.c +unrealdb.o: unrealdb.c $(INCLUDES) + $(CC) $(CFLAGS) $(BINCFLAGS) -c unrealdb.c + updconf.o: updconf.c $(INCLUDES) $(CC) $(CFLAGS) $(BINCFLAGS) -c updconf.c diff --git a/src/api-event.c b/src/api-event.c index 83cc0ddb3..8c328d227 100644 --- a/src/api-event.c +++ b/src/api-event.c @@ -28,6 +28,7 @@ ID_Copyright("(C) Carsten Munk 2001"); MODVAR Event *events = NULL; extern EVENT(unrealdns_removeoldrecords); +extern EVENT(unrealdb_expire_secret_cache); /** Add an event, a function that will run at regular intervals. * @param module Module that this event belongs to @@ -241,4 +242,5 @@ void SetupEvents(void) EventAdd(NULL, "handshake_timeout", handshake_timeout, NULL, 1000, 0); EventAdd(NULL, "try_connections", try_connections, NULL, 2000, 0); EventAdd(NULL, "tls_check_expiry", tls_check_expiry, NULL, (86400/2)*1000, 0); + EventAdd(NULL, "unrealdb_expire_secret_cache", unrealdb_expire_secret_cache, NULL, 61000, 0); } diff --git a/src/auth.c b/src/auth.c index d7f3300e9..39403a5f8 100644 --- a/src/auth.c +++ b/src/auth.c @@ -20,7 +20,6 @@ #include "unrealircd.h" #include "crypt_blowfish.h" -#include typedef struct AuthTypeList AuthTypeList; struct AuthTypeList { diff --git a/src/conf.c b/src/conf.c index 905abee0b..92a405469 100644 --- a/src/conf.c +++ b/src/conf.c @@ -71,6 +71,7 @@ static int _conf_help (ConfigFile *conf, ConfigEntry *ce); static int _conf_offchans (ConfigFile *conf, ConfigEntry *ce); static int _conf_sni (ConfigFile *conf, ConfigEntry *ce); static int _conf_security_group (ConfigFile *conf, ConfigEntry *ce); +static int _conf_secret (ConfigFile *conf, ConfigEntry *ce); /* * Validation commands @@ -104,6 +105,7 @@ static int _test_help (ConfigFile *conf, ConfigEntry *ce); static int _test_offchans (ConfigFile *conf, ConfigEntry *ce); static int _test_sni (ConfigFile *conf, ConfigEntry *ce); static int _test_security_group (ConfigFile *conf, ConfigEntry *ce); +static int _test_secret (ConfigFile *conf, ConfigEntry *ce); /* This MUST be alphabetized */ static ConfigCommand _ConfigCommands[] = { @@ -128,6 +130,7 @@ static ConfigCommand _ConfigCommands[] = { { "oper", _conf_oper, _test_oper }, { "operclass", _conf_operclass, _test_operclass }, { "require", _conf_require, _test_require }, + { "secret", _conf_secret, _test_secret }, { "security-group", _conf_security_group, _test_security_group }, { "set", _conf_set, _test_set }, { "sni", _conf_sni, _test_sni }, @@ -258,6 +261,7 @@ ConfigItem_blacklist_module *conf_blacklist_module = NULL; ConfigItem_help *conf_help = NULL; ConfigItem_offchans *conf_offchans = NULL; SecurityGroup *securitygroups = NULL; +Secret *secrets = NULL; MODVAR Configuration iConf; MODVAR Configuration tempiConf; @@ -2705,8 +2709,15 @@ int config_run() config_status("Running %s", cfptr->cf_filename); for (ce = cfptr->cf_entries; ce; ce = ce->ce_next) { - if (!strcmp(ce->ce_varname, "set") || !strcmp(ce->ce_varname, "class")) - continue; // already processed + /* These are already processed above (set, class) + * or via config_test() (secret). + */ + if (!strcmp(ce->ce_varname, "set") || + !strcmp(ce->ce_varname, "class") || + !strcmp(ce->ce_varname, "secret")) + { + continue; + } if ((cc = config_binary_search(ce->ce_varname))) { if ((cc->conffunc) && (cc->conffunc(cfptr, ce) < 0)) @@ -2793,6 +2804,17 @@ int config_test() { if (config_verbose > 1) config_status("Testing %s", cfptr->cf_filename); + /* First test and run the secret { } blocks */ + for (ce = cfptr->cf_entries; ce; ce = ce->ce_next) + { + if (!strcmp(ce->ce_varname, "secret")) + { + int n = _test_secret(cfptr, ce); + errors += n; + if (n == 0) + _conf_secret(cfptr, ce); + } + } /* First test the set { } block */ for (ce = cfptr->cf_entries; ce; ce = ce->ce_next) { @@ -2803,8 +2825,11 @@ int config_test() for (ce = cfptr->cf_entries; ce; ce = ce->ce_next) { /* These are already processed, so skip them here.. */ - if (!strcmp(ce->ce_varname, "set")) + if (!strcmp(ce->ce_varname, "secret") || + !strcmp(ce->ce_varname, "set")) + { continue; + } if ((cc = config_binary_search(ce->ce_varname))) { if (cc->testfunc) errors += (cc->testfunc(cfptr, ce)); @@ -10203,6 +10228,209 @@ int _conf_security_group(ConfigFile *conf, ConfigEntry *ce) return 1; } +Secret *find_secret(char *secret_name) +{ + Secret *s; + for (s = secrets; s; s = s->next) + { + if (!strcasecmp(s->name, secret_name)) + return s; + } + return NULL; +} + +void free_secret_cache(SecretCache *c) +{ + unrealdb_free_config(c->config); + safe_free(c); +} + +void free_secret(Secret *s) +{ + SecretCache *c, *c_next; + for (c = s->cache; c; c = c_next) + { + c_next = c->next; + DelListItem(c, s->cache); + free_secret_cache(c); + } + safe_free(s->name); + safe_free_sensitive(s->password); + safe_free(s); +} + +char *_conf_secret_read_password(char *fname) +{ + char *pwd, *err; + int fd, n; + +#ifndef _WIN32 + fd = open(fname, O_RDONLY); +#else + fd = open(fname, _O_RDONLY|_O_BINARY); +#endif + if (fd < 0) + { + /* This should not happen, as we tested for file exists earlier.. */ + config_error("Could not open file '%s': %s", fname, strerror(errno)); + return NULL; + } + + pwd = safe_alloc_sensitive(512); + n = read(fd, pwd, 511); + if (n <= 0) + { + close(fd); + config_error("Could not read from file '%s': %s", fname, strerror(errno)); + safe_free_sensitive(pwd); + return NULL; + } + close(fd); + stripcrlf(pwd); + sodium_stackzero(1024); + if (!valid_secret_password(pwd, &err)) + { + config_error("Key from file '%s' does not meet password complexity requirements: %s", fname, err); + safe_free_sensitive(pwd); + return NULL; + } + return pwd; +} + +int _test_secret(ConfigFile *conf, ConfigEntry *ce) +{ + int errors = 0; + int has_password = 0, has_password_file = 0; + ConfigEntry *cep; + char *err; + + if (!ce->ce_vardata) + { + config_error("%s:%i: secret block needs a name, eg: secret xyz {", + ce->ce_fileptr->cf_filename, ce->ce_varlinenum); + errors++; + } else { + if (!security_group_valid_name(ce->ce_vardata)) + { + config_error("%s:%i: secret block name '%s' contains invalid characters or is too long. " + "Only letters, numbers, underscore and hyphen are allowed.", + ce->ce_fileptr->cf_filename, ce->ce_varlinenum, ce->ce_vardata); + errors++; + } + } + + for (cep = ce->ce_entries; cep; cep = cep->ce_next) + { + if (!strcmp(cep->ce_varname, "password")) + { + has_password = 1; + CheckNull(cep); + if (!valid_secret_password(cep->ce_vardata, &err)) + { + config_error("%s:%d: secret::password does not meet password complexity requirements: %s", + cep->ce_fileptr->cf_filename, cep->ce_varlinenum, err); + errors++; + } + } else + if (!strcmp(cep->ce_varname, "password-file")) + { + char *str; + has_password_file = 1; + CheckNull(cep); + str = _conf_secret_read_password(cep->ce_vardata); + if (!str) + { + config_error("%s:%d: secret::password-file: error reading password from file, see error from above.", + cep->ce_fileptr->cf_filename, cep->ce_varlinenum); + errors++; + } + safe_free_sensitive(str); + } else + if (!strcmp(cep->ce_varname, "password-url")) + { + config_error("%s:%d: secret::password-url is not supported yet in this UnrealIRCd version.", + cep->ce_fileptr->cf_filename, cep->ce_varlinenum); + errors++; + } else + { + config_error_unknown(cep->ce_fileptr->cf_filename, cep->ce_varlinenum, + "secret", cep->ce_varname); + errors++; + continue; + } + if (cep->ce_entries) + { + config_error("%s:%d: secret::%s does not support sub-options (%s)", + cep->ce_fileptr->cf_filename, cep->ce_varlinenum, + cep->ce_varname, cep->ce_entries->ce_varname); + errors++; + } + } + + if (!has_password && !has_password_file) + { + config_error("%s:%d: secret { } block must contain 1 of: password OR password-file", + ce->ce_fileptr->cf_filename, ce->ce_varlinenum); + errors++; + } + + return errors; +} + +/* NOTE: contrary to all other _conf* stuff, this one actually runs during config_test, + * so during the early CONFIG TEST stage rather than CONFIG RUN. + * This so all secret { } block configuration is available already during TEST/POSTTEST + * stage for modules, so they can check if the password is correct or not. + */ +int _conf_secret(ConfigFile *conf, ConfigEntry *ce) +{ + ConfigEntry *cep; + Secret *s; + Secret *existing; + + s = safe_alloc(sizeof(Secret)); + safe_strdup(s->name, ce->ce_vardata); + + for (cep = ce->ce_entries; cep; cep = cep->ce_next) + { + if (!strcmp(cep->ce_varname, "password")) + { + safe_strdup_sensitive(s->password, cep->ce_vardata); + destroy_string(cep->ce_vardata); /* destroy the original */ + } else + if (!strcmp(cep->ce_varname, "password-file")) + { + s->password = _conf_secret_read_password(cep->ce_vardata); + } + } + + /* This may happen if we run twice, due to destroy_string() earlier: */ + if (BadPtr(s->password)) + { + free_secret(s); + return 1; + } + + /* If there is an existing secret { } block with this name in memory + * and it has a different password, then free that secret block + */ + if ((existing = find_secret(s->name))) + { + if (!strcmp(s->password, existing->password)) + { + free_secret(s); + return 1; + } + /* passwords differ, so free the old existing one, + * including purging the cache for it. + */ + DelListItem(existing, secrets); + free_secret(existing); + } + AddListItem(s, secrets); + return 1; +} + #ifdef USE_LIBCURL static void conf_download_complete(const char *url, const char *file, const char *errorbuf, int cached, void *inc_key) { diff --git a/src/ircd.c b/src/ircd.c index 3242924bc..d60787aca 100644 --- a/src/ircd.c +++ b/src/ircd.c @@ -61,6 +61,7 @@ static void open_debugfile(), setup_signals(); extern void init_glines(void); extern void tkl_init(void); extern void process_clients(void); +extern void unrealdb_test(void); #ifndef _WIN32 MODVAR char **myargv; @@ -928,6 +929,11 @@ int InitUnrealIRCd(int argc, char *argv[]) safe_strdup(configfile, CONFIGFILE); init_random(); /* needs to be done very early!! */ + if (sodium_init() < 0) + { + fprintf(stderr, "Failed to initialize sodium library -- error accessing random device?\n"); + exit(-1); + } memset(&botmotd, '\0', sizeof(MOTDFile)); memset(&rules, '\0', sizeof(MOTDFile)); @@ -1071,9 +1077,10 @@ int InitUnrealIRCd(int argc, char *argv[]) exit(0); } #endif -#if 0 +#if 1 case 'S': - charsys_dump_table(p ? p : "*"); + //charsys_dump_table(p ? p : "*"); + unrealdb_test(); exit(0); #endif #ifndef _WIN32 diff --git a/src/misc.c b/src/misc.c index e1410977c..1018fb4d6 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1974,3 +1974,68 @@ char *sendtype_to_cmd(SendType sendtype) return "TAGMSG"; return NULL; } + +/** Check password strength. + * @param pass The password to check + * @param min_length The minimum length of the password + * @param strict Whether to require UPPER+lower+digits + * @returns 1 if good, 0 if not. + */ +int check_password_strength(char *pass, int min_length, int strict, char **err) +{ + char has_lowercase=0, has_uppercase=0, has_digit=0; + char *p; + static char buf[256]; + + if (err) + *err = NULL; + + if (strlen(pass) < min_length) + { + if (err) + { + snprintf(buf, sizeof(buf), "Password must be at least %d characters", min_length); + *err = buf; + } + return 0; + } + + for (p=pass; *p; p++) + { + if (islower(*p)) + has_lowercase = 1; + else if (isupper(*p)) + has_uppercase = 1; + else if (isdigit(*p)) + has_digit = 1; + } + + if (strict) + { + if (!has_lowercase) + { + if (err) + *err = "Password must contain at least 1 lowercase character"; + return 0; + } else + if (!has_uppercase) + { + if (err) + *err = "Password must contain at least 1 UPPERcase character"; + return 0; + } else + if (!has_digit) + { + if (err) + *err = "Password must contain at least 1 digit (number)"; + return 0; + } + } + + return 1; +} + +int valid_secret_password(char *pass, char **err) +{ + return check_password_strength(pass, 10, 1, err); +} diff --git a/src/support.c b/src/support.c index ef7e4845d..a8eb54a76 100644 --- a/src/support.c +++ b/src/support.c @@ -724,6 +724,34 @@ void outofmemory(size_t bytes) exit(7); } +/** Allocate sensitive memory - this should only be used for HIGHLY sensitive data, since + * it wastes 8192+ bytes even if only asked to allocate for example 32 bytes (this is by design). + * @param size How many bytes to allocate + * @returns A pointer to the newly allocated memory. + * @note If out of memory then the IRCd will exit. + */ +void *safe_alloc_sensitive(size_t size) +{ + void *p; + if (size == 0) + return NULL; + p = sodium_malloc(((size/32)*32)+32); + if (!p) + outofmemory(size); + memset(p, 0, size); + return p; +} + +/** Safely duplicate a string */ +char *our_strdup_sensitive(const char *str) +{ + char *ret = safe_alloc_sensitive(strlen(str)+1); + if (!ret) + outofmemory(strlen(str)); + strcpy(ret, str); /* safe, see above */ + return ret; +} + /** Returns a unique filename in the specified directory * using the specified suffix. The returned value will * be of the form /. diff --git a/src/unrealdb.c b/src/unrealdb.c new file mode 100644 index 000000000..4851343aa --- /dev/null +++ b/src/unrealdb.c @@ -0,0 +1,1040 @@ +/************************************************************************ + * src/unrealdb.c + * Functions for dealing easily with (encrypted) database files. + * (C) Copyright 2021 Bram Matthys (Syzop) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 1, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + + +/** @file + * @brief UnrealDB API + * This provides functions for dealing with (encrypted) database files. + * Under the hood it uses libsodium's XChaCha20: + * https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20 + * On standard hardware as of 2021 speeds of 150-200 megabytes per second + * are achieved realisticly for both reading and writing encrypted + * database files. Of course, YMMV, depending on record sizes, CPU, + * and I/O speeds of the underlying hardware. + */ +#include "unrealircd.h" + +/* In UnrealIRCd 5.0.10 we don't write the v1 header yet for unencrypted + * database files, this so users using unencrypted can easily downgrade + * to 5.0.9 and lower should there be any need to do so. + * We DO support READING encypted, unencrypted v1, and unencrypted raw (v0) + * in 5.0.10, though. + * Presumably in 2022 or so we will stop writing v0 by default and change + * this #undef to a #define to write v1. + */ +#undef UNREALDB_WRITE_V1 + +/* If a key is specified, it must be this size */ +#define UNREALDB_KEY_LEN crypto_secretstream_xchacha20poly1305_KEYBYTES + +/** Default 'time cost' for Argon2id */ +#define UNREALDB_ARGON2_DEFAULT_TIME_COST 4 +/** Default 'memory cost' for Argon2id. Note that 15 means 1<<15=32M */ +#define UNREALDB_ARGON2_DEFAULT_MEMORY_COST 15 +/** Default 'parallelism cost' for Argon2id. */ +#define UNREALDB_ARGON2_DEFAULT_PARALLELISM_COST 2 + +/* Forward declarations - only used for internal (static) functions, of course */ +static SecretCache *find_secret_cache(Secret *secr, UnrealDBConfig *cfg); +static void unrealdb_add_to_secret_cache(Secret *secr, UnrealDBConfig *cfg); + +UnrealDBError unrealdb_last_error_code; +static char *unrealdb_last_error_string = NULL; + +/** Get the error string for last failed unrealdb operation. + * @returns The error string + * @note Use the return value only for displaying of errors + * to the end-user. + * For programmatically checking of error conditions + * use unrealdb_get_error_code() instead. + * + */ +char *unrealdb_get_error_string(void) +{ + return unrealdb_last_error_string; +} + +/** Get the error code for last failed unrealdb operation + * @returns An UNREAL_DB_ERROR_*, enum of type UnrealDBError. + */ +UnrealDBError unrealdb_get_error_code(void) +{ + return unrealdb_last_error_code; +} + +/** Set error condition on unrealdb 'c' (internal function). + * @param c The unrealdb file handle + * @param pattern The format string + * @param ... Any parameters to the format string + * @note this will also set c->failed=1 to prevent any further reading/writing. + */ +static void unrealdb_set_error(UnrealDB *c, UnrealDBError errcode, FORMAT_STRING(const char *pattern), ...) +{ + va_list vl; + char buf[512]; + va_start(vl, pattern); + vsnprintf(buf, sizeof(buf), pattern, vl); + va_end(vl); + if (c) + { + c->error_code = errcode; + safe_strdup(c->error_string, buf); + } + unrealdb_last_error_code = errcode; + safe_strdup(unrealdb_last_error_string, buf); +} + +/** Free a UnrealDB struct (internal function). */ +static void unrealdb_free(UnrealDB *c) +{ + unrealdb_free_config(c->config); + safe_free(c->error_string); + safe_free_sensitive(c); +} + +static int unrealdb_kdf(UnrealDB *c, Secret *secr) +{ + if (c->config->kdf != UNREALDB_KDF_ARGON2ID) + { + unrealdb_set_error(c, UNREALDB_ERROR_INTERNAL, "Unknown KDF 0x%x", (int)c->config->kdf); + return 0; + } + /* Need to run argon2 to generate key */ + if (argon2id_hash_raw(c->config->t_cost, + 1 << c->config->m_cost, + c->config->p_cost, + secr->password, strlen(secr->password), + c->config->salt, c->config->saltlen, + c->config->key, c->config->keylen) != ARGON2_OK) + { + /* out of memory or some other very unusual error */ + unrealdb_set_error(c, UNREALDB_ERROR_INTERNAL, "Could not generate argon2 hash - out of memory or something weird?"); + return 0; + } + return 1; +} + +/** Open an unrealdb file. + * @param filename The filename to open + * @param mode Either UNREALDB_MODE_READ or UNREALDB_MODE_WRITE + * @param secret_block The name of the secret xx { } block (so NOT the actual password!!) + * @returns A pointer to a UnrealDB structure that can be used in subsequent calls for db read/writes, + * and finally unrealdb_close(). Or NULL in case of failure. + * @note Upon error (NULL return value) you can call unrealdb_get_error_code() and + * unrealdb_get_error_string() to see the actual error. + */ +UnrealDB *unrealdb_open(const char *filename, UnrealDBMode mode, char *secret_block) +{ + UnrealDB *c = safe_alloc_sensitive(sizeof(UnrealDB)); + char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; + char buf[32]; /* don't change this */ + Secret *secr; + SecretCache *dbcache; + int cached = 0; + char *err; + + errno = 0; + + if ((mode != UNREALDB_MODE_READ) && (mode != UNREALDB_MODE_WRITE)) + { + unrealdb_set_error(c, UNREALDB_ERROR_API, "unrealdb_open request for neither read nor write"); + goto unrealdb_open_fail; + } + + c->mode = mode; + c->fd = fopen(filename, (c->mode == UNREALDB_MODE_WRITE) ? "wb" : "rb"); + if (!c->fd) + { + if (errno == ENOENT) + unrealdb_set_error(c, UNREALDB_ERROR_FILENOTFOUND, "File not found: %s", strerror(errno)); + else + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Could not open file: %s", strerror(errno)); + goto unrealdb_open_fail; + } + + if (secret_block == NULL) + { + if (mode == UNREALDB_MODE_READ) + { + /* READ: read header, if any, lots of fallback options here... */ + if (fgets(buf, sizeof(buf), c->fd)) + { + if (!strncmp(buf, "UnrealIRCd-DB-Crypted", 21)) + { + unrealdb_set_error(c, UNREALDB_ERROR_CRYPTED, "file is encrypted but no password provided"); + goto unrealdb_open_fail; + } else + if (!strcmp(buf, "UnrealIRCd-DB-v1")) + { + /* Skip over the 32 byte header, directly to the creationtime */ + if (fseek(c->fd, 32L, SEEK_SET) < 0) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "file header too short"); + goto unrealdb_open_fail; + } + if (!unrealdb_read_int64(c, &c->creationtime)) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header is too short (A4)"); + goto unrealdb_open_fail; + } + /* SUCCESS = fallthrough */ + } else + if (!strncmp(buf, "UnrealIRCd-DB", 13)) /* any other version than v1 = not supported by us */ + { + /* We don't support this format, so refuse clearly */ + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, + "Unsupported version of database. Is this database perhaps created on " + "a new version of UnrealIRCd and are you trying to use it on an older " + "UnrealIRCd version? (Downgrading is not supported!)"); + goto unrealdb_open_fail; + } else + { + /* Old db format, no header, seek back to beginning */ + fseek(c->fd, 0L, SEEK_SET); + /* SUCCESS = fallthrough */ + } + } + } else { +#ifdef UNREALDB_WRITE_V1 + /* WRITE */ + memset(buf, 0, sizeof(buf)); + snprintf(buf, sizeof(buf), "UnrealIRCd-DB-v1"); + if ((fwrite(buf, 1, sizeof(buf), c->fd) != sizeof(buf)) || + !unrealdb_write_int64(c, TStime())) + { + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Unable to write header (A1)"); + goto unrealdb_open_fail; + } +#endif + } + safe_free(unrealdb_last_error_string); + unrealdb_last_error_code = UNREALDB_ERROR_SUCCESS; + return c; + } + + c->crypted = 1; + + secr = find_secret(secret_block); + if (!secr) + { + unrealdb_set_error(c, UNREALDB_ERROR_SECRET, "Secret block '%s' not found or invalid", secr->name); + goto unrealdb_open_fail; + } + + if (!valid_secret_password(secr->password, &err)) + { + unrealdb_set_error(c, UNREALDB_ERROR_SECRET, "Password in secret block '%s' does not meet complexity requirements", secr->name); + goto unrealdb_open_fail; + } + + if (c->mode == UNREALDB_MODE_WRITE) + { + /* Write the: + * - generic header ("UnrealIRCd-DB" + some zeroes) + * - the salt + * - the crypto header + */ + memset(buf, 0, sizeof(buf)); + snprintf(buf, sizeof(buf), "UnrealIRCd-DB-Crypted-v1"); + if (fwrite(buf, 1, sizeof(buf), c->fd) != sizeof(buf)) + { + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Unable to write header (1)"); + goto unrealdb_open_fail; /* Unable to write header nr 1 */ + } + + if (secr->cache && secr->cache->config) + { + /* Use first found cached config for this secret */ + c->config = unrealdb_copy_config(secr->cache->config); + cached = 1; + } else { + /* Create a new config */ + c->config = safe_alloc(sizeof(UnrealDBConfig)); + c->config->kdf = UNREALDB_KDF_ARGON2ID; + c->config->t_cost = UNREALDB_ARGON2_DEFAULT_TIME_COST; + c->config->m_cost = UNREALDB_ARGON2_DEFAULT_MEMORY_COST; + c->config->p_cost = UNREALDB_ARGON2_DEFAULT_PARALLELISM_COST; + c->config->saltlen = UNREALDB_SALT_LEN; + c->config->salt = safe_alloc(c->config->saltlen); + randombytes_buf(c->config->salt, c->config->saltlen); + c->config->cipher = UNREALDB_CIPHER_XCHACHA20; + c->config->keylen = UNREALDB_KEY_LEN; + c->config->key = safe_alloc_sensitive(c->config->keylen); + } + + /* Write KDF and cipher parameters */ + if ((fwrite(&c->config->kdf, 1, sizeof(c->config->kdf), c->fd) != sizeof(c->config->kdf)) || + (fwrite(&c->config->t_cost, 1, sizeof(c->config->t_cost), c->fd) != sizeof(c->config->t_cost)) || + (fwrite(&c->config->m_cost, 1, sizeof(c->config->m_cost), c->fd) != sizeof(c->config->m_cost)) || + (fwrite(&c->config->p_cost, 1, sizeof(c->config->p_cost), c->fd) != sizeof(c->config->p_cost)) || + (fwrite(&c->config->saltlen, 1, sizeof(c->config->saltlen), c->fd) != sizeof(c->config->saltlen)) || + (fwrite(c->config->salt, 1, c->config->saltlen, c->fd) != c->config->saltlen) || + (fwrite(&c->config->cipher, 1, sizeof(c->config->cipher), c->fd) != sizeof(c->config->cipher)) || + (fwrite(&c->config->keylen, 1, sizeof(c->config->keylen), c->fd) != sizeof(c->config->keylen))) + { + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Unable to write header (2)"); + goto unrealdb_open_fail; + } + + if (cached) + { +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "[UnrealDB] unrealdb_open(): Cache hit for '%s' while writing", secr->name); +#endif + } else + { +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "[UnrealDB] unrealdb_open(): Need to run argon2 '%s' while writing", secr->name); +#endif + if (!unrealdb_kdf(c, secr)) + { + /* Error already set by called function */ + goto unrealdb_open_fail; + } + } + + crypto_secretstream_xchacha20poly1305_init_push(&c->st, header, c->config->key); + if (fwrite(header, 1, sizeof(header), c->fd) != sizeof(header)) + { + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Unable to write header (3)"); + goto unrealdb_open_fail; /* Unable to write crypto header */ + } + if (!unrealdb_write_str(c, "UnrealIRCd-DB-Crypted-Now") || + !unrealdb_write_int64(c, TStime())) + { + /* error is already set by unrealdb_write_str() */ + goto unrealdb_open_fail; /* Unable to write crypto header */ + } + if (!cached) + unrealdb_add_to_secret_cache(secr, c->config); + } else + { + char *validate = NULL; + + /* Read file header */ + if (fread(buf, 1, sizeof(buf), c->fd) != sizeof(buf)) + { + unrealdb_set_error(c, UNREALDB_ERROR_NOTCRYPTED, "Not a crypted file (file too small)"); + goto unrealdb_open_fail; /* Header too short */ + } + if (strncmp(buf, "UnrealIRCd-DB-Crypted-v1", 24)) + { + unrealdb_set_error(c, UNREALDB_ERROR_NOTCRYPTED, "Not a crypted file"); + goto unrealdb_open_fail; /* Invalid header */ + } + c->config = safe_alloc(sizeof(UnrealDBConfig)); + if ((fread(&c->config->kdf, 1, sizeof(c->config->kdf), c->fd) != sizeof(c->config->kdf)) || + (fread(&c->config->t_cost, 1, sizeof(c->config->t_cost), c->fd) != sizeof(c->config->t_cost)) || + (fread(&c->config->m_cost, 1, sizeof(c->config->m_cost), c->fd) != sizeof(c->config->m_cost)) || + (fread(&c->config->p_cost, 1, sizeof(c->config->p_cost), c->fd) != sizeof(c->config->p_cost)) || + (fread(&c->config->saltlen, 1, sizeof(c->config->saltlen), c->fd) != sizeof(c->config->saltlen))) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header is corrupt/unknown/invalid"); + goto unrealdb_open_fail; + } + if (c->config->kdf != UNREALDB_KDF_ARGON2ID) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header contains unknown KDF 0x%x", (int)c->config->kdf); + goto unrealdb_open_fail; + } + if (c->config->cipher != UNREALDB_CIPHER_XCHACHA20) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header contains unknown cipher 0x%x", (int)c->config->cipher); + goto unrealdb_open_fail; + } + if (c->config->saltlen > 1024) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header is corrupt (saltlen=%d)", (int)c->config->saltlen); + goto unrealdb_open_fail; /* Something must be wrong, this makes no sense. */ + } + c->config->salt = safe_alloc(c->config->saltlen); + if (fread(c->config->salt, 1, c->config->saltlen, c->fd) != c->config->saltlen) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header is too short (2)"); + goto unrealdb_open_fail; /* Header too short (read II) */ + } + if ((fread(&c->config->cipher, 1, sizeof(c->config->cipher), c->fd) != sizeof(c->config->cipher)) || + (fread(&c->config->keylen, 1, sizeof(c->config->keylen), c->fd) != sizeof(c->config->keylen))) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header is corrupt/unknown/invalid (3)"); + goto unrealdb_open_fail; + } + c->config->key = safe_alloc_sensitive(c->config->keylen); + + dbcache = find_secret_cache(secr, c->config); + if (dbcache) + { + /* Use cached key, no need to run expensive argon2.. */ + memcpy(c->config->key, dbcache->config->key, c->config->keylen); +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "[UnrealDB] unrealdb_open(): Cache hit for '%s' while reading", secr->name); +#endif + } else { +#ifdef DEBUGMODE + ircd_log(LOG_ERROR, "[UnrealDB] unrealdb_open(): Need to run argon2 for '%s' while reading", secr->name); +#endif + if (!unrealdb_kdf(c, secr)) + { + /* Error already set by called function */ + goto unrealdb_open_fail; + } + } + /* key is now set */ + if (fread(header, 1, sizeof(header), c->fd) != sizeof(header)) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header is too short (3)"); + goto unrealdb_open_fail; /* Header too short */ + } + if (crypto_secretstream_xchacha20poly1305_init_pull(&c->st, header, c->config->key) != 0) + { + unrealdb_set_error(c, UNREALDB_ERROR_PASSWORD, "Crypto error - invalid password or corrupt file"); + goto unrealdb_open_fail; /* Unusual */ + } + /* Now to validate the key we read a simple string */ + if (!unrealdb_read_str(c, &validate)) + { + unrealdb_set_error(c, UNREALDB_ERROR_PASSWORD, "Invalid password"); + goto unrealdb_open_fail; /* Incorrect key, probably */ + } + if (strcmp(validate, "UnrealIRCd-DB-Crypted-Now")) + { + safe_free(validate); + unrealdb_set_error(c, UNREALDB_ERROR_PASSWORD, "Invalid password"); + goto unrealdb_open_fail; /* Incorrect key, probably */ + } + safe_free(validate); + if (!unrealdb_read_int64(c, &c->creationtime)) + { + unrealdb_set_error(c, UNREALDB_ERROR_HEADER, "Header is too short (4)"); + goto unrealdb_open_fail; + } + unrealdb_add_to_secret_cache(secr, c->config); + } + sodium_stackzero(1024); + safe_free(unrealdb_last_error_string); + unrealdb_last_error_code = UNREALDB_ERROR_SUCCESS; + return c; + +unrealdb_open_fail: + if (c->fd) + fclose(c->fd); + unrealdb_free(c); + sodium_stackzero(1024); + return NULL; +} + +/** Close an unrealdb file. + * @param c The struct pointing to an unrealdb file + * @returns 1 if the final close was graceful and 0 if not (eg: out of disk space on final flush). + * In all cases the file handle is closed and 'c' is freed. + */ +int unrealdb_close(UnrealDB *c) +{ + /* If this is file was opened for writing then flush the remaining data with a TAG_FINAL + * (or push a block of 0 bytes with TAG_FINAL) + */ + if (c->crypted && (c->mode == UNREALDB_MODE_WRITE)) + { + char buf_out[UNREALDB_CRYPT_FILE_CHUNK_SIZE + crypto_secretstream_xchacha20poly1305_ABYTES]; + unsigned long long out_len = sizeof(buf_out); + + crypto_secretstream_xchacha20poly1305_push(&c->st, buf_out, &out_len, c->buf, c->buflen, NULL, 0, crypto_secretstream_xchacha20poly1305_TAG_FINAL); + if (out_len > 0) + { + if (fwrite(buf_out, 1, out_len, c->fd) != out_len) + { + /* Final write failed, error condition */ + fclose(c->fd); + unrealdb_free(c); + return 0; + } + } + } + + if (fclose(c->fd) != 0) + { + /* Final close failed, error condition */ + unrealdb_free(c); + return 0; + } + + unrealdb_free(c); + return 1; +} + +/** Test if there is something fatally wrong with the configuration of the DB file, + * in which case we suggest to reject the /rehash or boot request. + * This tests for "wrong password" and for "trying to open an encrypted file without providing a password" + * which are clear configuration errors on the admin part. + * It does NOT test for any other conditions such as missing file, corrupted file, etc. + * since that usually needs different handling anyway, as they are I/O issues and don't + * always have a clear solution (if any is needed at all). + * @param filename The filename to open + * @param secret_block The name of the secret xx { } block (so NOT the actual password!!) + * @returns 1 if the password was wrong, 0 for any other error or succes. + */ +char *unrealdb_test_db(const char *filename, char *secret_block) +{ + static char buf[512]; + UnrealDB *db = unrealdb_open(filename, UNREALDB_MODE_READ, secret_block); + if (!db) + { + if (unrealdb_get_error_code() == UNREALDB_ERROR_PASSWORD) + { + snprintf(buf, sizeof(buf), "Incorrect password specified in secret block '%s' for file %s", + secret_block, filename); + return buf; + } + if (unrealdb_get_error_code() == UNREALDB_ERROR_CRYPTED) + { + snprintf(buf, sizeof(buf), "File '%s' is encrypted but no secret block provided for it", + filename); + return buf; + } + return NULL; + } else + { + unrealdb_close(db); + } + return NULL; +} + +/** Write to an unrealdb file. + * This code uses extra buffering to avoid writing small records + * and wasting for example a 32 bytes encryption block for a 8 byte write request. + * @param c Database file open for writing + * @param buf The data to be written (plaintext) + * @param len The length of the data to be written + * @note This is the internal function, api users must use unrealdb_read_data() instead. + */ +static int unrealdb_write(UnrealDB *c, void *buf, int len) +{ + char buf_out[UNREALDB_CRYPT_FILE_CHUNK_SIZE + crypto_secretstream_xchacha20poly1305_ABYTES]; + unsigned long long out_len; + + if (c->error_code) + return 0; + + if (c->mode != UNREALDB_MODE_WRITE) + { + unrealdb_set_error(c, UNREALDB_ERROR_API, "Write operation requested on a file opened for reading"); + return 0; + } + + if (!c->crypted) + return fwrite(buf, 1, len, c->fd); + + do { + if (c->buflen + len < UNREALDB_CRYPT_FILE_CHUNK_SIZE) + { + /* New data fits in new buffer. Then we are done with writing. + * This can happen both for the first block (never write) + * or the remainder (tail after X writes which is less than + * UNREALDB_CRYPT_FILE_CHUNK_SIZE, a common case) + */ + memcpy(c->buf + c->buflen, buf, len); + c->buflen += len; + break; /* Done! */ + } else + { + /* Fill up c->buf with UNREALDB_CRYPT_FILE_CHUNK_SIZE + * Note that 'av_bytes' can be 0 here if c->buflen + * happens to be exactly UNREALDB_CRYPT_FILE_CHUNK_SIZE, + * that's okay. + */ + int av_bytes = UNREALDB_CRYPT_FILE_CHUNK_SIZE - c->buflen; + if (av_bytes > 0) + memcpy(c->buf + c->buflen, buf, av_bytes); + buf += av_bytes; + len -= av_bytes; + } + if (crypto_secretstream_xchacha20poly1305_push(&c->st, buf_out, &out_len, c->buf, UNREALDB_CRYPT_FILE_CHUNK_SIZE, NULL, 0, 0) != 0) + { + unrealdb_set_error(c, UNREALDB_ERROR_INTERNAL, "Failed to encrypt a block"); + return 0; + } + if (fwrite(buf_out, 1, out_len, c->fd) != out_len) + { + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Write error: %s", strerror(errno)); + return 0; + } + /* Buffer is now flushed for sure */ + c->buflen = 0; + } while(len > 0); + + return 1; +} + +/** Write data to an unrealdb file. + * @param c Database file open for writing + * @param buf The data to be written (plaintext) + * @param len The length of the data to be written + */ +int unrealdb_write_data(UnrealDB *c, void *buf, int len) +{ + return unrealdb_write(c, buf, len); +} + +/** Write a string to a database file. + * @param c UnrealDB file struct + * @param x String to be written + * @note This function can write a string up to 65534 + * characters, which should be plenty for usage + * in UnrealIRCd. + * Note that 'x' can safely be NULL. + * @returns 1 on success, 0 on failure. + */ +int unrealdb_write_str(UnrealDB *c, char *x) +{ + uint16_t len; + + len = x ? strlen(x) : 0xffff; + if (!unrealdb_write(c, &len, sizeof(len))) + return 0; + if ((len > 0) && (len < 0xffff)) + { + if (!unrealdb_write(c, x, len)) + return 0; + } + return 1; +} + +/** Write a 64 bit integer to a database file. + * @param c UnrealDB file struct + * @param t The value to write + * @returns 1 on success, 0 on failure. + */ +int unrealdb_write_int64(UnrealDB *c, uint64_t t) +{ + return unrealdb_write(c, &t, sizeof(t)); +} + +/** Write a 32 bit integer to a database file. + * @param c UnrealDB file struct + * @param t The value to write + * @returns 1 on success, 0 on failure. + */ +int unrealdb_write_int32(UnrealDB *c, uint32_t t) +{ + return unrealdb_write(c, &t, sizeof(t)); +} + +/** Write a 16 bit integer to a database file. + * @param c UnrealDB file struct + * @param t The value to write + * @returns 1 on success, 0 on failure. + */ +int unrealdb_write_int16(UnrealDB *c, uint16_t t) +{ + return unrealdb_write(c, &t, sizeof(t)); +} + +/** Read from an UnrealDB file. + * This code deals with buffering, block reading, etc. so the caller doesn't + * have to worry about that. + * @param c Database file open for reading + * @param buf The data to be read (will be plaintext) + * @param len The length of the data to be read + * @note This is the internal function, api users must use unrealdb_read_data() instead. + */ +static int unrealdb_read(UnrealDB *c, void *buf, int len) +{ + char buf_in[UNREALDB_CRYPT_FILE_CHUNK_SIZE + crypto_secretstream_xchacha20poly1305_ABYTES]; + unsigned long long out_len; + unsigned char tag; + size_t rlen; + + if (c->error_code) + return 0; + + if (c->mode != UNREALDB_MODE_READ) + { + unrealdb_set_error(c, UNREALDB_ERROR_API, "Read operation requested on a file opened for writing"); + return 0; + } + + if (!c->crypted) + return fread(buf, 1, len, c->fd); + + /* First, fill 'buf' up with what we have */ + if (c->buflen) + { + int av_bytes = MIN(c->buflen, len); + memcpy(buf, c->buf, av_bytes); + if (c->buflen - av_bytes > 0) + memmove(c->buf, c->buf + av_bytes, c->buflen - av_bytes); + c->buflen -= av_bytes; + len -= av_bytes; + if (len == 0) + return 1; /* Request completed entirely */ + buf += av_bytes; + } + + if (c->buflen != 0) + abort(); + + /* If we get here then we need to read some data */ + do { + rlen = fread(buf_in, 1, sizeof(buf_in), c->fd); + if (rlen == 0) + { + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Short read - premature end of file??"); + return 0; + } + if (crypto_secretstream_xchacha20poly1305_pull(&c->st, c->buf, &out_len, &tag, buf_in, rlen, NULL, 0) != 0) + { + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Failed to decrypt a block - either corrupt or wrong key"); + return 0; + } + + if (len > UNREALDB_CRYPT_FILE_CHUNK_SIZE) + { + memcpy(buf, c->buf, UNREALDB_CRYPT_FILE_CHUNK_SIZE); + buf += UNREALDB_CRYPT_FILE_CHUNK_SIZE; + len -= UNREALDB_CRYPT_FILE_CHUNK_SIZE; + } else { + memcpy(buf, c->buf, len); + c->buflen = UNREALDB_CRYPT_FILE_CHUNK_SIZE - len; + if (c->buflen > 0) + memmove(c->buf, c->buf+len, c->buflen); + return 1; /* Done */ + } + } while(!feof(c->fd)); + + unrealdb_set_error(c, UNREALDB_ERROR_IO, "Short read - premature end of file?"); + return 0; +} + +/** Read data from an UnrealDB file. + * @param c Database file open for reading + * @param buf The data to be read (will be plaintext) + * @param len The length of the data to be read + */ +int unrealdb_read_data(UnrealDB *c, void *buf, int len) +{ + return unrealdb_read(c, buf, len); +} + +/** Read a 64 bit integer from a database file. + * @param c UnrealDB file struct + * @param t The value to read + * @returns 1 on success, 0 on failure. + */ +int unrealdb_read_int64(UnrealDB *c, uint64_t *t) +{ + if (!unrealdb_read(c, t, sizeof(uint64_t))) + return 0; + return 1; +} + +/** Read a 32 bit integer from a database file. + * @param c UnrealDB file struct + * @param t The value to read + * @returns 1 on success, 0 on failure. + */ +int unrealdb_read_int32(UnrealDB *c, uint32_t *t) +{ + if (!unrealdb_read(c, t, sizeof(uint32_t))) + return 0; + return 1; +} + +/** Read a 16 bit integer from a database file. + * @param c UnrealDB file struct + * @param t The value to read + * @returns 1 on success, 0 on failure. + */ +int unrealdb_read_int16(UnrealDB *c, uint16_t *t) +{ + if (!unrealdb_read(c, t, sizeof(uint16_t))) + return 0; + return 1; +} + +/** Read a string from a database file. + * @param c UnrealDB file struct + * @param x Pointer to string pointer + * @note This function will allocate memory for the data + * and set the string pointer to this value. + * If a NULL pointer was written via write_str() + * then read_str() may also return a NULL pointer. + * @returns 1 on success, 0 on failure. + */ +int unrealdb_read_str(UnrealDB *c, char **x) +{ + uint16_t len; + size_t size; + + *x = NULL; + + if (!unrealdb_read(c, &len, sizeof(len))) + return 0; + + if (len == 0xffff) + { + /* Magic value meaning NULL */ + *x = NULL; + return 1; + } + + if (len == 0) + { + /* 0 means empty string */ + safe_strdup(*x, ""); + return 1; + } + + if (len > 10000) + return 0; + + size = len; + *x = safe_alloc(size + 1); + if (!unrealdb_read(c, *x, size)) + { + safe_free(*x); + return 0; + } + (*x)[len] = 0; + return 1; +} + +void fatal_error(FORMAT_STRING(const char *pattern), ...) +{ + va_list vl; + va_start(vl, pattern); + vfprintf(stderr, pattern, vl); + va_end(vl); + fprintf(stderr, "\n"); + fprintf(stderr, "Exiting with failure\n"); + exit(-1); +} + +void unrealdb_test_simple(void) +{ + UnrealDB *c; + char *key = "test"; + int i; + char *str; + + + fprintf(stderr, "*** WRITE TEST ***\n"); + c = unrealdb_open("/tmp/test.db", UNREALDB_MODE_WRITE, key); + if (!c) + fatal_error("Could not open test db for writing: %s", strerror(errno)); + + if (!unrealdb_write_str(c, "Hello world!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")) + fatal_error("Error on write 1"); + if (!unrealdb_write_str(c, "This is a test!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")) + fatal_error("Error on write 2"); + if (!unrealdb_close(c)) + fatal_error("Error on close"); + c = NULL; + fprintf(stderr, "Done with writing.\n\n"); + + fprintf(stderr, "*** READ TEST ***\n"); + c = unrealdb_open("/tmp/test.db", UNREALDB_MODE_READ, key); + if (!c) + fatal_error("Could not open test db for reading: %s", strerror(errno)); + if (!unrealdb_read_str(c, &str)) + fatal_error("Error on read 1: %s", c->error_string); + fprintf(stderr, "Got: '%s'\n", str); + safe_free(str); + if (!unrealdb_read_str(c, &str)) + fatal_error("Error on read 2: %s", c->error_string); + fprintf(stderr, "Got: '%s'\n", str); + safe_free(str); + if (!unrealdb_close(c)) + fatal_error("Error on close"); + fprintf(stderr, "All good.\n"); +} + +#define UNREALDB_SPEED_TEST_BYTES 100000000 +void unrealdb_test_speed(char *key) +{ + UnrealDB *c; + int i, len; + char *str; + char buf[1024]; + int written = 0, read = 0; + struct timeval tv_start, tv_end; + + fprintf(stderr, "*** WRITE TEST ***\n"); + gettimeofday(&tv_start, NULL); + c = unrealdb_open("/tmp/test.db", UNREALDB_MODE_WRITE, key); + if (!c) + fatal_error("Could not open test db for writing: %s", strerror(errno)); + do { + + len = getrandom32() % 500; + //gen_random_alnum(buf, len); + for (i=0; i < len; i++) + buf[i] = 'a'; + buf[i] = '\0'; + if (!unrealdb_write_str(c, buf)) + fatal_error("Error on writing a string of %d size", len); + written += len + 2; /* +2 for length */ + } while(written < UNREALDB_SPEED_TEST_BYTES); + if (!unrealdb_close(c)) + fatal_error("Error on close"); + c = NULL; + gettimeofday(&tv_end, NULL); + fprintf(stderr, "Done with writing: %lld usecs\n\n", + (long long)(((tv_end.tv_sec - tv_start.tv_sec) * 1000000) + (tv_end.tv_usec - tv_start.tv_usec))); + + fprintf(stderr, "*** READ TEST ***\n"); + gettimeofday(&tv_start, NULL); + c = unrealdb_open("/tmp/test.db", UNREALDB_MODE_READ, key); + if (!c) + fatal_error("Could not open test db for reading: %s", strerror(errno)); + do { + if (!unrealdb_read_str(c, &str)) + fatal_error("Error on read at position %d/%d: %s", read, written, c->error_string); + read += strlen(str) + 2; /* same calculation as earlier */ + safe_free(str); + } while(read < written); + if (!unrealdb_close(c)) + fatal_error("Error on close"); + gettimeofday(&tv_end, NULL); + fprintf(stderr, "Done with reading: %lld usecs\n\n", + (long long)(((tv_end.tv_sec - tv_start.tv_sec) * 1000000) + (tv_end.tv_usec - tv_start.tv_usec))); + + fprintf(stderr, "All good.\n"); +} + +void unrealdb_test(void) +{ + //unrealdb_test_simple(); + fprintf(stderr, "**** TESTING ENCRYPTED ****\n"); + unrealdb_test_speed("test"); + fprintf(stderr, "**** TESTING UNENCRYPTED ****\n"); + unrealdb_test_speed(NULL); +} + +char *unrealdb_test_secret(char *name) +{ + // FIXME: check if exists, if not then return an error, with a nice FAQ reference etc. + return NULL; /* no error */ +} + +UnrealDBConfig *unrealdb_copy_config(UnrealDBConfig *src) +{ + UnrealDBConfig *dst = safe_alloc(sizeof(UnrealDBConfig)); + + dst->saltlen = src->saltlen; + dst->salt = safe_alloc(dst->saltlen); + memcpy(dst->salt, src->salt, dst->saltlen); + + dst->keylen = src->keylen; + if (dst->keylen) + { + dst->key = safe_alloc_sensitive(dst->keylen); + memcpy(dst->key, src->key, dst->keylen); + } + + dst->t_cost = src->t_cost; + dst->m_cost = src->m_cost; + dst->p_cost = src->p_cost; + return dst; +} + +UnrealDBConfig *unrealdb_get_config(UnrealDB *db) +{ + return unrealdb_copy_config(db->config); +} + +void unrealdb_free_config(UnrealDBConfig *c) +{ + if (!c) + return; + safe_free(c->salt); + safe_free_sensitive(c->key); + safe_free(c); +} + +static int unrealdb_config_identical(UnrealDBConfig *one, UnrealDBConfig *two) +{ + /* NOTE: do not compare 'key' here or all cache lookups will fail */ + if ((one->kdf == two->kdf) && + (one->t_cost == two->t_cost) && + (one->m_cost == two->m_cost) && + (one->p_cost == two->p_cost) && + (one->saltlen == two->saltlen) && + (memcmp(one->salt, two->salt, one->saltlen) == 0) && + (one->cipher == two->cipher) && + (one->keylen == two->keylen)) + { + return 1; + } + return 0; +} + +static SecretCache *find_secret_cache(Secret *secr, UnrealDBConfig *cfg) +{ + SecretCache *c; + + for (c = secr->cache; c; c = c->next) + { + if (unrealdb_config_identical(c->config, cfg)) + { + c->cache_hit = TStime(); + return c; + } + } + return NULL; +} + +static void unrealdb_add_to_secret_cache(Secret *secr, UnrealDBConfig *cfg) +{ + SecretCache *c = find_secret_cache(secr, cfg); + + if (c) + return; /* Entry already exists in cache */ + + /* New entry, add! */ + c = safe_alloc(sizeof(SecretCache)); + c->config = unrealdb_copy_config(cfg); + c->cache_hit = TStime(); + AddListItem(c, secr->cache); +} + +#ifdef DEBUGMODE +#define UNREALDB_EXPIRE_SECRET_CACHE_AFTER 1200 +#else +#define UNREALDB_EXPIRE_SECRET_CACHE_AFTER 86400 +#endif + +/** Expire cached secret entries (previous Argon2 runs) */ +EVENT(unrealdb_expire_secret_cache) +{ + Secret *s; + SecretCache *c, *c_next; + for (s = secrets; s; s = s->next) + { + for (c = s->cache; c; c = c_next) + { + c_next = c->next; + if (c->cache_hit < TStime() - UNREALDB_EXPIRE_SECRET_CACHE_AFTER) + { + DelListItem(c, s->cache); + free_secret_cache(c); + } + } + } +}