Use PBKDF2 with HMAC-SHA1 for master passphrase in clawsrc.
authorAndrej Kacian <ticho@claws-mail.org>
Thu, 7 Apr 2016 12:56:48 +0000 (14:56 +0200)
committerAndrej Kacian <ticho@claws-mail.org>
Fri, 8 Apr 2016 08:05:37 +0000 (10:05 +0200)
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
src/password.c
src/prefs_common.c
src/prefs_common.h

index 76a8c9e..4709f55 100644 (file)
@@ -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'.
index f24229b..ac8fe50 100644 (file)
@@ -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"
 #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
index 8895b5f..f4acf34 100644 (file)
@@ -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}
index 8a38e42..e1c2df9 100644 (file)
@@ -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
 };