From b15be3734e3722d9df38188bcfb2617118b1c66b Mon Sep 17 00:00:00 2001 From: samr7 Date: Thu, 11 Aug 2011 13:43:03 -0700 Subject: [PATCH] Add private key password-protection. --- oclvanitygen.c | 25 +++- pattern.c | 25 +++- pattern.h | 1 + util.c | 311 +++++++++++++++++++++++++++++++++++++++++++++++++ util.h | 8 ++ vanitygen.c | 25 +++- 6 files changed, 388 insertions(+), 7 deletions(-) diff --git a/oclvanitygen.c b/oclvanitygen.c index a3b9eec..b9085cc 100644 --- a/oclvanitygen.c +++ b/oclvanitygen.c @@ -2248,6 +2248,8 @@ usage(const char *name) "-N Generate namecoin address\n" "-T Generate bitcoin testnet address\n" "-X Generate address with the given version\n" +"-e Encrypt private keys, prompt for password\n" +"-E Encrypt private keys with (UNSAFE)\n" "-p Select OpenCL platform\n" "-d Select OpenCL device\n" "-S Safe mode, disable OpenCL loop unrolling optimizations\n" @@ -2270,7 +2272,9 @@ main(int argc, char **argv) int regex = 0; int caseinsensitive = 0; int opt; + char pwbuf[128]; int platformidx = -1, deviceidx = -1; + int prompt_password = 0; char *seedfile = NULL; FILE *fp = NULL; char **patterns, *pend; @@ -2285,9 +2289,10 @@ main(int argc, char **argv) vg_context_t *vcp = NULL; cl_device_id did; const char *result_file = NULL; + const char *key_password = NULL; while ((opt = getopt(argc, argv, - "vqrikNTX:p:d:w:t:g:b:Sh?f:o:s:")) != -1) { + "vqrikNTX:eE:p:d:w:t:g:b:Sh?f:o:s:")) != -1) { switch (opt) { case 'v': verbose = 2; @@ -2316,6 +2321,12 @@ main(int argc, char **argv) addrtype = atoi(optarg); privtype = 128 + addrtype; break; + case 'e': + prompt_password = 1; + break; + case 'E': + key_password = optarg; + break; case 'p': platformidx = atoi(optarg); break; @@ -2467,6 +2478,18 @@ main(int argc, char **argv) return 1; } + if (prompt_password) { + if (!vg_read_password(pwbuf, sizeof(pwbuf))) + return 1; + key_password = pwbuf; + } + vcp->vc_key_protect_pass = key_password; + if (key_password) { + if (!vg_check_password_complexity(key_password, verbose)) + printf("WARNING: Protecting private keys with " + "weak password\n"); + } + if ((verbose > 0) && regex && (vcp->vc_npatterns > 1)) printf("Regular expressions: %ld\n", vcp->vc_npatterns); diff --git a/pattern.c b/pattern.c index 7c1e4e3..61e7e19 100644 --- a/pattern.c +++ b/pattern.c @@ -306,11 +306,26 @@ vg_output_match(vg_context_t *vcp, EC_KEY *pkey, const char *pattern) unsigned char key_buf[512], *pend; char addr_buf[64]; char privkey_buf[128]; + const char *keytype = "Privkey"; int len; assert(EC_KEY_check_key(pkey)); vg_encode_address(pkey, vcp->vc_addrtype, addr_buf); - vg_encode_privkey(pkey, vcp->vc_privtype, privkey_buf); + + if (vcp->vc_key_protect_pass) { + len = vg_protect_encode_privkey(privkey_buf, + pkey, vcp->vc_privtype, + vcp->vc_key_protect_pass); + if (len) { + keytype = "Protkey"; + } else { + printf("ERROR: could not password-protect key\n"); + vcp->vc_key_protect_pass = NULL; + } + } + if (!vcp->vc_key_protect_pass) { + vg_encode_privkey(pkey, vcp->vc_privtype, privkey_buf); + } if (!vcp->vc_result_file || (vcp->vc_verbose > 0)) { printf("\r%79s\rPattern: %s\n", "", pattern); @@ -334,8 +349,8 @@ vg_output_match(vg_context_t *vcp, EC_KEY *pkey, const char *pattern) if (!vcp->vc_result_file || (vcp->vc_verbose > 0)) { printf("Address: %s\n" - "Privkey: %s\n", - addr_buf, privkey_buf); + "%s: %s\n", + addr_buf, keytype, privkey_buf); } if (vcp->vc_result_file) { @@ -347,8 +362,8 @@ vg_output_match(vg_context_t *vcp, EC_KEY *pkey, const char *pattern) fprintf(fp, "Pattern: %s\n" "Address: %s\n" - "Privkey: %s\n", - pattern, addr_buf, privkey_buf); + "%s: %s\n", + pattern, addr_buf, keytype, privkey_buf); fclose(fp); } } diff --git a/pattern.h b/pattern.h index b594539..5d3cf38 100644 --- a/pattern.h +++ b/pattern.h @@ -75,6 +75,7 @@ struct _vg_context_s { unsigned long long vc_found; double vc_chance; const char *vc_result_file; + const char *vc_key_protect_pass; int vc_remove_on_match; int vc_verbose; vg_free_func_t vc_free; diff --git a/util.c b/util.c index 2302bac..53b7636 100644 --- a/util.c +++ b/util.c @@ -20,10 +20,14 @@ #include #include #include +#include #include #include #include +#include +#include +#include #include "pattern.h" #include "util.h" @@ -284,6 +288,313 @@ vg_decode_privkey(const char *b58encoded, EC_KEY *pkey, int *addrtype) return res; } +#define VG_PROTKEY_SALT_SIZE 4 +#define VG_PROTKEY_HMAC_SIZE 8 +#define VG_PROTKEY_HMAC_KEY_SIZE 16 + +static int +vg_protect_setup(EVP_CIPHER_CTX *ctx, unsigned char *hmac_out, + const char *pass, const unsigned char *salt, int enc) +{ + unsigned char keymaterial[EVP_MAX_KEY_LENGTH + EVP_MAX_IV_LENGTH + + VG_PROTKEY_HMAC_KEY_SIZE]; + const EVP_CIPHER *cipher; + + cipher = EVP_aes_256_cbc(); + + PKCS5_PBKDF2_HMAC((const char *) pass, strlen(pass) + 1, + salt, VG_PROTKEY_SALT_SIZE, + 4096, + EVP_sha256(), + cipher->key_len + cipher->iv_len + + VG_PROTKEY_HMAC_KEY_SIZE, + keymaterial); + + if (!EVP_CipherInit(ctx, cipher, + keymaterial, + keymaterial + cipher->key_len, + enc)) { + OPENSSL_cleanse(keymaterial, sizeof(keymaterial)); + printf("ERROR: could not configure cipher\n"); + return 0; + } + + EVP_CIPHER_CTX_set_padding(ctx, 0); + + memcpy(hmac_out, + keymaterial + cipher->key_len + cipher->iv_len, + VG_PROTKEY_HMAC_KEY_SIZE); + + OPENSSL_cleanse(keymaterial, sizeof(keymaterial)); + return 1; +} + +int +vg_protect_encode_privkey(char *out, + const EC_KEY *pkey, int keytype, + const char *pass) +{ + unsigned char ecpriv[64]; + unsigned char ecenc[64]; + unsigned char hmac[EVP_MAX_MD_SIZE]; + unsigned char salt[VG_PROTKEY_SALT_SIZE]; + unsigned char hmac_key[VG_PROTKEY_HMAC_KEY_SIZE]; + const BIGNUM *privkey; + EVP_CIPHER_CTX *ctx = NULL; + unsigned int hlen; + int opos, olen, oincr, nbytes; + + privkey = EC_KEY_get0_private_key(pkey); + nbytes = BN_num_bytes(privkey); + if (nbytes < 32) + memset(ecpriv, 0, 32 - nbytes); + BN_bn2bin(privkey, ecpriv + 32 - nbytes); + + ctx = EVP_CIPHER_CTX_new(); + + /* + * The string representation of this protected key is + * ridiculously long. To save a few bytes, we will only + * add four unique random bytes to the salt, out of the + * eight mandated by PBKDF. This should not reduce its + * effectiveness. + */ + RAND_bytes(salt, VG_PROTKEY_SALT_SIZE); + + if (!vg_protect_setup(ctx, hmac_key, pass, salt, 1)) { + EVP_CIPHER_CTX_free(ctx); + return 0; + } + + hlen = sizeof(hmac); + HMAC(EVP_sha256(), + hmac_key, VG_PROTKEY_HMAC_KEY_SIZE, + ecpriv, 32, + hmac, &hlen); + + OPENSSL_cleanse(hmac_key, sizeof(hmac_key)); + + ecenc[0] = 136; + opos = 1; + olen = sizeof(ecenc) - opos; + + oincr = olen; + EVP_EncryptUpdate(ctx, ecenc + opos, &oincr, ecpriv, 32); + opos += oincr; + olen -= oincr; + + oincr = olen; + EVP_EncryptFinal(ctx, ecenc + opos, &oincr); + opos += oincr; + + EVP_CIPHER_CTX_free(ctx); + + memcpy(ecenc + opos, hmac, VG_PROTKEY_HMAC_SIZE); + opos += VG_PROTKEY_HMAC_SIZE; + + memcpy(ecenc + opos, salt, VG_PROTKEY_SALT_SIZE); + opos += VG_PROTKEY_SALT_SIZE; + + vg_b58_encode_check(ecenc, opos, out); + nbytes = strlen(out); + assert(nbytes == 67); + return nbytes; +} + + +int +vg_protect_decode_privkey(EC_KEY *pkey, int *keytype, + const char *encoded, const char *pass) +{ + unsigned char ecpriv[64]; + unsigned char ecenc[64]; + unsigned char hmac[EVP_MAX_MD_SIZE]; + unsigned char salt[VG_PROTKEY_SALT_SIZE]; + unsigned char hmac_key[VG_PROTKEY_HMAC_KEY_SIZE]; + EVP_CIPHER_CTX *ctx = NULL; + BIGNUM bn; + unsigned int hlen; + int opos, olen, oincr; + int res; + + res = vg_b58_decode_check(encoded, ecenc, sizeof(ecenc)); + if (res != 45) + return 0; + + memcpy(salt, ecenc + 41, VG_PROTKEY_SALT_SIZE); + + ctx = EVP_CIPHER_CTX_new(); + + if (!vg_protect_setup(ctx, hmac_key, pass, salt, 0)) { + EVP_CIPHER_CTX_free(ctx); + return 0; + } + + opos = 0; + olen = sizeof(ecenc) - opos; + oincr = olen; + EVP_DecryptUpdate(ctx, ecpriv + opos, &oincr, ecenc + 1, 32); + opos += oincr; + olen -= oincr; + + oincr = olen; + EVP_DecryptFinal(ctx, ecpriv + opos, &oincr); + opos += oincr; + + EVP_CIPHER_CTX_free(ctx); + + hlen = sizeof(hmac); + HMAC(EVP_sha256(), + hmac_key, VG_PROTKEY_HMAC_KEY_SIZE, + ecpriv, 32, + hmac, &hlen); + + if (memcmp(ecenc + 33, hmac, VG_PROTKEY_HMAC_SIZE)) { + OPENSSL_cleanse(ecpriv, sizeof(ecpriv)); + printf("ERROR: invalid password\n"); + return 0; + } + + BN_init(&bn); + BN_bin2bn(ecpriv, 32, &bn); + res = vg_set_privkey(&bn, pkey); + BN_clear_free(&bn); + OPENSSL_cleanse(ecpriv, sizeof(ecpriv)); + + if (res) { + switch(ecenc[0]) { + case 136: + *keytype = 128; + break; + default: + printf("Unrecognized private key type\n"); + res = 0; + break; + } + } + return res; +} + +int +vg_read_password(char *buf, size_t size) +{ + return !EVP_read_pw_string(buf, size, "Enter new password:", 1); +} + + +/* + * Password complexity checker + * Heavily inspired by, but a simplification of "How Secure Is My Password?", + * http://howsecureismypassword.net/ + */ +static unsigned char ascii_class[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 5, 4, 5, 4, 4, 4, 4, 5, 4, 4, 4, 4, 5, 4, 5, 5, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 5, 5, 4, 5, 5, + 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 5, 5, 5, 4, 4, + 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 5, 0, +}; + +int +vg_check_password_complexity(const char *pass, int verbose) +{ + int i, len; + int classes[6] = { 0, }; + const char *crackunit = "seconds"; + int char_complexity = 0; + double crackops, cracktime; + int weak; + + /* + * This number reflects a resourceful attacker with + * USD >$20K in 2011 hardware + */ + const int rate = 250000000; + + /* Consider the password weak if it can be cracked in <1 year */ + const int weak_threshold = (60*60*24*365); + + len = strlen(pass); + for (i = 0; i < len; i++) { + if (pass[i] > sizeof(ascii_class)) + /* FIXME: skip the rest of the UTF8 char */ + classes[5]++; + else if (!ascii_class[(int)pass[i]]) + continue; + else + classes[(int)ascii_class[(int)pass[i]] - 1]++; + } + + if (classes[0]) + char_complexity += 26; + if (classes[1]) + char_complexity += 26; + if (classes[2]) + char_complexity += 10; + if (classes[3]) + char_complexity += 14; + if (classes[4]) + char_complexity += 19; + if (classes[5]) + char_complexity += 32; /* oversimplified */ + + /* This assumes brute-force and oversimplifies the problem */ + crackops = pow((double)char_complexity, (double)len); + cracktime = (crackops * (1 - (1/M_E))) / rate; + weak = (cracktime < weak_threshold); + + if (cracktime > 60.0) { + cracktime /= 60.0; + crackunit = "minutes"; + if (cracktime > 60.0) { + cracktime /= 60.0; + crackunit = "hours"; + if (cracktime > 24.0) { + cracktime /= 24; + crackunit = "days"; + if (cracktime > 365.0) { + cracktime /= 365.0; + crackunit = "years"; + } + } + } + } + + /* Complain by default about weak passwords */ + if ((weak && (verbose > 0)) || (verbose > 1)) { + if (cracktime < 1.0) { + printf("Estimated password crack time: >1 %s\n", + crackunit); + } else if (cracktime < 1000000) { + printf("Estimated password crack time: %.1f %s\n", + cracktime, crackunit); + } else { + printf("Estimated password crack time: %e %s\n", + cracktime, crackunit); + } + if (!classes[0] && !classes[1] && classes[2] && + !classes[3] && !classes[4] && !classes[5]) { + printf("WARNING: Password contains only numbers\n"); + } + else if (!classes[2] && !classes[3] && !classes[4] && + !classes[5]) { + if (!classes[0] || !classes[1]) { + printf("WARNING: Password contains " + "only %scase letters\n", + classes[0] ? "lower" : "upper"); + } else { + printf("WARNING: Password contains " + "only letters\n"); + } + } + } + + return !weak; +} + /* * Pattern file reader diff --git a/util.h b/util.h index c85fab1..6d3d71c 100644 --- a/util.h +++ b/util.h @@ -39,6 +39,14 @@ extern int vg_set_privkey(const BIGNUM *bnpriv, EC_KEY *pkey); extern int vg_decode_privkey(const char *b58encoded, EC_KEY *pkey, int *addrtype); +extern int vg_protect_encode_privkey(char *out, + const EC_KEY *pkey, int keytype, + const char *pass); +extern int vg_protect_decode_privkey(EC_KEY *pkey, int *keytype, + const char *encoded, const char *pass); + +extern int vg_read_password(char *buf, size_t size); +extern int vg_check_password_complexity(const char *pass, int verbose); extern int vg_read_file(FILE *fp, char ***result, int *rescount); diff --git a/vanitygen.c b/vanitygen.c index e0dfbbc..6c36373 100644 --- a/vanitygen.c +++ b/vanitygen.c @@ -449,6 +449,8 @@ usage(const char *name) "-N Generate namecoin address\n" "-T Generate bitcoin testnet address\n" "-X Generate address with the given version\n" +"-e Encrypt private keys, prompt for password\n" +"-E Encrypt private keys with (UNSAFE)\n" "-t Set number of worker threads (Default: number of CPUs)\n" "-f File containing list of patterns, one per line\n" " (Use \"-\" as the file name for stdin)\n" @@ -466,16 +468,19 @@ main(int argc, char **argv) int caseinsensitive = 0; int verbose = 1; int remove_on_match = 1; + int prompt_password = 0; int opt; char *seedfile = NULL; FILE *fp = NULL; + char pwbuf[128]; const char *result_file = NULL; + const char *key_password = NULL; char **patterns; int npatterns = 0; int nthreads = 0; vg_context_t *vcp = NULL; - while ((opt = getopt(argc, argv, "vqrikNTX:t:h?f:o:s:")) != -1) { + while ((opt = getopt(argc, argv, "vqrikeE:NTX:t:h?f:o:s:")) != -1) { switch (opt) { case 'v': verbose = 2; @@ -504,6 +509,12 @@ main(int argc, char **argv) addrtype = atoi(optarg); privtype = 128 + addrtype; break; + case 'e': + prompt_password = 1; + break; + case 'E': + key_password = optarg; + break; case 't': nthreads = atoi(optarg); if (nthreads == 0) { @@ -615,6 +626,18 @@ main(int argc, char **argv) return 1; } + if (prompt_password) { + if (!vg_read_password(pwbuf, sizeof(pwbuf))) + return 1; + key_password = pwbuf; + } + vcp->vc_key_protect_pass = key_password; + if (key_password) { + if (!vg_check_password_complexity(key_password, verbose)) + printf("WARNING: Protecting private keys with " + "weak password\n"); + } + if ((verbose > 0) && regex && (vcp->vc_npatterns > 1)) printf("Regular expressions: %ld\n", vcp->vc_npatterns);