From ffd418aaa7b4bdf401193a96194346ff7e403b9e Mon Sep 17 00:00:00 2001 From: Andrej Kacian Date: Thu, 7 Apr 2016 14:56:48 +0200 Subject: [PATCH] Use PBKDF2 with HMAC-SHA1 for master passphrase in clawsrc. The 64 bytes long key derivation is stored in 'master_passphrase' pref, together with number of rounds used in its computation. Introducing also two new common prefs: master_passphrase_salt - holds a randomly generated 64 bytes for use as salt with PBKDF2. Base64-encoded. master_passphrase_pbkdf2_rounds - number of rounds (or iterations) for next passphrase key derivation The latter can be tweaked by user in case they want to use more or less rounds, e.g. if they're running on weaker hardware and KD with default number of rounds takes too long. --- doc/src/password_encryption.txt | 18 +++- src/password.c | 167 +++++++++++++++++++++++++------- src/prefs_common.c | 4 +- src/prefs_common.h | 4 +- 4 files changed, 155 insertions(+), 38 deletions(-) diff --git a/doc/src/password_encryption.txt b/doc/src/password_encryption.txt index 76a8c9e7f..4709f557d 100644 --- a/doc/src/password_encryption.txt +++ b/doc/src/password_encryption.txt @@ -16,7 +16,6 @@ IV for the cipher is filled with random bytes. Encryption ---------- - We prepare a buffer 128+blocksize bytes long, with one block of random data at the beginning, followed by the password we want to encrypt, rest is padded with zero bytes. @@ -40,8 +39,23 @@ with our password. Why the random block at the beginning? -------------------------------------- - We are taking advantage of property of CBC mode where decryption with a wrong IV results in only first block being garbled. Therefore we prepend a random block to our plaintext before encryption, and discard first block from plaintext after decryption. + + +Master passphrase +----------------- +This can be any string user chooses. We store its 64 bytes long key +derivation (KD), using PBKDF2 with HMAC-SHA1, and later check correctness +of user-entered passphrase by making same KD from it and comparing it +to the stored one. Only if the two KDs match, the passphrase is accepted +and remembered for session, thus giving access to account or plugin +passwords. + +Salt used for PBKDF2 is stored in 'master_passphrase_salt', encoded +as base64. It consists of 64 randomly generated bytes. + +Number of rounds for PBKDF2 is stored in hidden preference +'master_passphrase_pbkdf2_rounds'. diff --git a/src/password.c b/src/password.c index f24229b45..ac8fe5032 100644 --- a/src/password.c +++ b/src/password.c @@ -40,6 +40,7 @@ #include "common/passcrypt.h" #include "common/plugin.h" +#include "common/pkcs5_pbkdf2.h" #include "common/utils.h" #include "account.h" #include "alertpanel.h" @@ -51,6 +52,87 @@ #ifndef PASSWORD_CRYPTO_OLD static gchar *_master_passphrase = NULL; +/* Length of stored key derivation, before base64. */ +#define KD_LENGTH 64 + +/* Length of randomly generated and saved salt, used for key derivation. + * Also before base64. */ +#define KD_SALT_LENGTH 64 + +static void _generate_salt() +{ +#if defined G_OS_UNIX + int rnd; +#elif defined G_OS_WIN32 + HCRYPTPROV rnd; +#endif + gint ret; + guchar salt[KD_SALT_LENGTH]; + + if (prefs_common_get_prefs()->master_passphrase_salt != NULL) { + g_free(prefs_common_get_prefs()->master_passphrase_salt); + } + + /* Prepare our source of random data. */ +#if defined G_OS_UNIX + rnd = open("/dev/urandom", O_RDONLY); + if (rnd == -1) { + perror("fopen on /dev/urandom"); +#elif defined G_OS_WIN32 + if (!CryptAcquireContext(&rnd, NULL, NULL, PROV_RSA_FULL, 0) && + !CryptAcquireContext(&rnd, NULL, NULL, PROV_RSA_FULL, CRYPT_NEWKEYSET)) { + debug_print("Could not acquire a CSP handle.\n"); +#endif + return; + } + +#if defined G_OS_UNIX + ret = read(rnd, salt, KD_SALT_LENGTH); + if (ret != KD_SALT_LENGTH) { + perror("read into salt"); + close(rnd); +#elif defined G_OS_WIN32 + if (!CryptGenRandom(rnd, KD_SALT_LENGTH, salt)) { + debug_print("Could not read random data for salt\n"); + CryptReleaseContext(rnd, 0); +#endif + return; + } + + prefs_common_get_prefs()->master_passphrase_salt = + g_base64_encode(salt, KD_SALT_LENGTH); +} + +#undef KD_SALT_LENGTH + +static guchar *_make_key_deriv(const gchar *passphrase, guint rounds) +{ + guchar *kd, *salt; + gchar *saltpref = prefs_common_get_prefs()->master_passphrase_salt; + gsize saltlen; + gint ret; + + /* Grab our salt, generating and saving a new random one if needed. */ + if (saltpref == NULL || strlen(saltpref) == 0) { + _generate_salt(); + saltpref = prefs_common_get_prefs()->master_passphrase_salt; + } + salt = g_base64_decode(saltpref, &saltlen); + kd = g_malloc0(KD_LENGTH); + + ret = pkcs5_pbkdf2(passphrase, strlen(passphrase), salt, saltlen, + kd, KD_LENGTH, rounds); + + g_free(salt); + + if (ret == 0) { + return kd; + } + + g_free(kd); + return NULL; +} + static const gchar *master_passphrase() { gchar *input; @@ -88,8 +170,8 @@ static const gchar *master_passphrase() const gboolean master_passphrase_is_set() { - if (prefs_common_get_prefs()->master_passphrase_hash == NULL - || strlen(prefs_common_get_prefs()->master_passphrase_hash) == 0) + if (prefs_common_get_prefs()->master_passphrase == NULL + || strlen(prefs_common_get_prefs()->master_passphrase) == 0) return FALSE; return TRUE; @@ -97,40 +179,52 @@ const gboolean master_passphrase_is_set() const gboolean master_passphrase_is_correct(const gchar *input) { - gchar *hash; + guchar *kd, *input_kd; gchar **tokens; - gchar *stored_hash = prefs_common_get_prefs()->master_passphrase_hash; - const GChecksumType hashtype = G_CHECKSUM_SHA256; - const gssize hashlen = g_checksum_type_get_length(hashtype); - gssize stored_len; + gchar *stored_kd = prefs_common_get_prefs()->master_passphrase; + gsize kd_len; + guint rounds = 0; + gint ret; + g_return_val_if_fail(stored_kd != NULL && strlen(stored_kd) > 0, FALSE); g_return_val_if_fail(input != NULL, FALSE); - if (stored_hash == NULL) + if (stored_kd == NULL) return FALSE; - tokens = g_strsplit_set(stored_hash, "{}", 3); - if (strlen(tokens[0]) != 0 || - strcmp(tokens[1], "SHA-256") || - strlen(tokens[2]) == 0) { - debug_print("Mangled master_passphrase_hash in config, can not use it.\n"); + tokens = g_strsplit_set(stored_kd, "{}", 3); + if (tokens[0] == NULL || + strlen(tokens[0]) != 0 || /* nothing before { */ + tokens[1] == NULL || + strncmp(tokens[1], "PBKDF2-HMAC-SHA1,", 17) || /* correct tag */ + strlen(tokens[1]) <= 17 || /* something after , */ + (rounds = atoi(tokens[1] + 17)) <= 0 || /* valid rounds # */ + tokens[2] == NULL || + strlen(tokens[2]) == 0) { /* string continues after } */ + debug_print("Mangled master_passphrase format in config, can not use it.\n"); g_strfreev(tokens); return FALSE; } - stored_hash = tokens[2]; - stored_len = strlen(stored_hash); - g_return_val_if_fail(stored_len == 2*hashlen, FALSE); + stored_kd = tokens[2]; + kd = g_base64_decode(stored_kd, &kd_len); /* should be 64 */ + g_strfreev(tokens); - hash = g_compute_checksum_for_string(hashtype, input, -1); + if (kd_len != KD_LENGTH) { + debug_print("master_passphrase is %ld bytes long, should be %d.\n", + kd_len, KD_LENGTH); + g_free(kd); + return FALSE; + } - if (!strncasecmp(hash, stored_hash, stored_len)) { - g_free(hash); - g_strfreev(tokens); + input_kd = _make_key_deriv(input, rounds); + ret = memcmp(kd, input_kd, kd_len); + + g_free(input_kd); + g_free(kd); + + if (ret == 0) return TRUE; - } - g_strfreev(tokens); - g_free(hash); return FALSE; } @@ -153,8 +247,11 @@ void master_passphrase_forget() void master_passphrase_change(const gchar *oldp, const gchar *newp) { - const GChecksumType hashtype = G_CHECKSUM_SHA256; - gchar *hash; + guchar *kd; + gchar *base64_kd; + guint rounds = prefs_common_get_prefs()->master_passphrase_pbkdf2_rounds; + + g_return_if_fail(rounds > 0); if (oldp == NULL) { /* If oldp is NULL, make sure the user has to enter the @@ -165,18 +262,20 @@ void master_passphrase_change(const gchar *oldp, const gchar *newp) g_return_if_fail(oldp != NULL); /* Update master passphrase hash in prefs */ - if (prefs_common_get_prefs()->master_passphrase_hash != NULL) - g_free(prefs_common_get_prefs()->master_passphrase_hash); + if (prefs_common_get_prefs()->master_passphrase != NULL) + g_free(prefs_common_get_prefs()->master_passphrase); if (newp != NULL) { - debug_print("Storing hash of new master passphrase\n"); - hash = g_compute_checksum_for_string(hashtype, newp, -1); - prefs_common_get_prefs()->master_passphrase_hash = - g_strconcat("{SHA-256}", hash, NULL); - g_free(hash); + debug_print("Storing key derivation of new master passphrase\n"); + kd = _make_key_deriv(newp, rounds); + base64_kd = g_base64_encode(kd, 64); + prefs_common_get_prefs()->master_passphrase = + g_strdup_printf("{PBKDF2-HMAC-SHA1,%d}%s", rounds, base64_kd); + g_free(kd); + g_free(base64_kd); } else { - debug_print("Setting master_passphrase_hash to NULL\n"); - prefs_common_get_prefs()->master_passphrase_hash = NULL; + debug_print("Setting master_passphrase to NULL\n"); + prefs_common_get_prefs()->master_passphrase = NULL; } /* Now go over all accounts, reencrypting their passwords using diff --git a/src/prefs_common.c b/src/prefs_common.c index 8895b5f08..f4acf34c0 100644 --- a/src/prefs_common.c +++ b/src/prefs_common.c @@ -1191,7 +1191,9 @@ static PrefParam param[] = { {"enable_avatars", "3", &prefs_common.enable_avatars, P_INT, NULL, NULL, NULL}, #ifndef PASSWORD_CRYPTO_OLD {"use_master_passphrase", FALSE, &prefs_common.use_master_passphrase, P_BOOL, NULL, NULL, NULL }, - {"master_passphrase_hash", "", &prefs_common.master_passphrase_hash, P_STRING, NULL, NULL, NULL }, + {"master_passphrase", "", &prefs_common.master_passphrase, P_STRING, NULL, NULL, NULL }, + {"master_passphrase_salt", "", &prefs_common.master_passphrase_salt, P_STRING, NULL, NULL, NULL }, + {"master_passphrase_pbkdf2_rounds", "50000", &prefs_common.master_passphrase_pbkdf2_rounds, P_INT, NULL, NULL, NULL}, #endif {NULL, NULL, NULL, P_OTHER, NULL, NULL, NULL} diff --git a/src/prefs_common.h b/src/prefs_common.h index 8a38e42d5..e1c2df993 100644 --- a/src/prefs_common.h +++ b/src/prefs_common.h @@ -541,7 +541,9 @@ struct _PrefsCommon #ifndef PASSWORD_CRYPTO_OLD gboolean use_master_passphrase; - gchar *master_passphrase_hash; + gchar *master_passphrase; + gchar *master_passphrase_salt; + guint master_passphrase_pbkdf2_rounds; #endif }; -- 2.25.1