1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-09 14:03:12 +02:00

Add unrealdb and secrets API. Documentation and more information will

follow in later commits.
This commit is contained in:
Bram Matthys
2021-05-03 15:07:10 +02:00
parent dd33b38264
commit dde3e0ccb2
11 changed files with 1517 additions and 8 deletions
+51
View File
@@ -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);
+86
View File
@@ -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 <sodium.h>
/* 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)
+1 -1
View File
@@ -35,4 +35,4 @@
#ifdef USE_LIBCURL
#include <curl/curl.h>
#endif
#include <sodium.h>
#include <argon2.h>
+4 -1
View File
@@ -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
+2
View File
@@ -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);
}
-1
View File
@@ -20,7 +20,6 @@
#include "unrealircd.h"
#include "crypt_blowfish.h"
#include <argon2.h>
typedef struct AuthTypeList AuthTypeList;
struct AuthTypeList {
+231 -3
View File
@@ -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)
{
+9 -2
View File
@@ -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
+65
View File
@@ -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);
}
+28
View File
@@ -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 <dir>/<random-hex>.<suffix>
+1040
View File
File diff suppressed because it is too large Load Diff