1 /** 2 * Passhash9 Password Hashing 3 * 4 * Copyright: 5 * (C) 2010 Jack Lloyd 6 * (C) 2014-2015 Etienne Cimon 7 * 8 * License: 9 * Botan is released under the Simplified BSD License (see LICENSE.md) 10 */ 11 module botan.passhash.passhash9; 12 13 import botan.constants; 14 static if (BOTAN_HAS_PASSHASH9 && BOTAN_HAS_PBKDF2): 15 16 import botan.rng.rng; 17 import botan.utils.loadstor; 18 import botan.libstate.libstate; 19 import botan.pbkdf.pbkdf2; 20 import botan.filters.b64_filt; 21 import botan.filters.pipe; 22 import botan.utils.get_byte; 23 import botan.utils.exceptn; 24 import std..string : toStringz; 25 import botan.codec.base64; 26 27 /** 28 * Create a password hash using PBKDF2 29 * Params: 30 * pass = the password 31 * rng = a random number generator 32 * work_factor = how much work to do to slow down guessing attacks 33 * alg_id = specifies which PRF to use with PBKDF2 34 * 0 is HMAC(SHA-1) 35 * 1 is HMAC(SHA-256) 36 * 2 is CMAC(Blowfish) 37 * 3 is HMAC(SHA-384) 38 * 4 is HMAC(SHA-512) 39 * all other values are currently undefined 40 */ 41 string generatePasshash9(in string pass, 42 RandomNumberGenerator rng, 43 ushort work_factor = 10, 44 ubyte alg_id = 1) 45 { 46 MessageAuthenticationCode prf = getPbkdfPrf(alg_id); 47 48 if (!prf) 49 throw new InvalidArgument("Passhash9: Algorithm id " ~ to!string(alg_id) ~ " is not defined"); 50 51 auto kdf = scoped!PKCS5_PBKDF2(prf); // takes ownership of pointer 52 53 SecureVector!ubyte salt = SecureVector!ubyte(SALT_BYTES); 54 rng.randomize(salt.ptr, salt.length); 55 56 const size_t kdf_iterations = WORK_FACTOR_SCALE * work_factor; 57 58 SecureVector!ubyte blob; 59 blob ~= alg_id; 60 blob ~= get_byte(0, work_factor); 61 blob ~= get_byte(1, work_factor); 62 blob ~= salt; 63 blob ~= kdf.deriveKey(PASSHASH9_PBKDF_OUTPUT_LEN, 64 pass, 65 salt.ptr, salt.length, 66 kdf_iterations).bitsOf(); 67 return MAGIC_PREFIX ~ base64Encode(blob); 68 } 69 70 71 /** 72 * Check a previously created password hash 73 * Params: 74 * password = the password to check against 75 * hash = the stored hash to check against 76 */ 77 bool checkPasshash9(in string password, in string hash) 78 { 79 __gshared immutable size_t BINARY_LENGTH = ALGID_BYTES + WORKFACTOR_BYTES + PASSHASH9_PBKDF_OUTPUT_LEN + SALT_BYTES; 80 81 __gshared immutable size_t BASE64_LENGTH = MAGIC_PREFIX.length + (BINARY_LENGTH * 8) / 6; 82 83 if (hash.length != BASE64_LENGTH) 84 return false; 85 86 for (size_t i = 0; i != MAGIC_PREFIX.length; ++i) 87 if (hash[i] != MAGIC_PREFIX[i]) 88 return false; 89 90 SecureVector!ubyte bin = base64Decode(hash.ptr[MAGIC_PREFIX.length .. hash.length]); 91 92 if (bin.length != BINARY_LENGTH) 93 return false; 94 95 ubyte alg_id = bin[0]; 96 97 const size_t work_factor = loadBigEndian!ushort(&bin[ALGID_BYTES], 0); 98 // Bug in the format, bad states shouldn't be representable, but are... 99 if (work_factor == 0) 100 return false; 101 102 if (work_factor > 512) 103 throw new InvalidArgument("Requested Bcrypt work factor " ~ to!string(work_factor) ~ " too large"); 104 105 const size_t kdf_iterations = WORK_FACTOR_SCALE * work_factor; 106 107 MessageAuthenticationCode pbkdf_prf = getPbkdfPrf(alg_id); 108 109 logTrace("Using ", pbkdf_prf.name, " work_factor: ", work_factor); 110 if (!pbkdf_prf) 111 return false; // unknown algorithm, reject 112 113 auto kdf = scoped!PKCS5_PBKDF2(pbkdf_prf); // takes ownership of pointer 114 115 SecureVector!ubyte cmp = kdf.deriveKey(PASSHASH9_PBKDF_OUTPUT_LEN, password, 116 bin.ptr + ALGID_BYTES + WORKFACTOR_BYTES, SALT_BYTES, 117 kdf_iterations).bitsOf(); 118 119 return sameMem(cmp.ptr, bin.ptr + ALGID_BYTES + WORKFACTOR_BYTES + SALT_BYTES, PASSHASH9_PBKDF_OUTPUT_LEN); 120 } 121 122 private: 123 124 __gshared immutable string MAGIC_PREFIX = "$9$"; 125 __gshared immutable size_t WORKFACTOR_BYTES = 2; 126 __gshared immutable size_t ALGID_BYTES = 1; 127 __gshared immutable size_t SALT_BYTES = 12; // 96 bits of salt 128 __gshared immutable size_t PASSHASH9_PBKDF_OUTPUT_LEN = 24; // 192 bits output 129 __gshared immutable size_t WORK_FACTOR_SCALE = 10000; 130 131 MessageAuthenticationCode getPbkdfPrf(ubyte alg_id) 132 { 133 AlgorithmFactory af = globalState().algorithmFactory(); 134 135 try 136 { 137 if (alg_id == 0) 138 return af.makeMac("HMAC(SHA-1)"); 139 else if (alg_id == 1) 140 return af.makeMac("HMAC(SHA-256)"); 141 else if (alg_id == 2) 142 return af.makeMac("CMAC(Blowfish)"); 143 else if (alg_id == 3) 144 return af.makeMac("HMAC(SHA-384)"); 145 else if (alg_id == 4) 146 return af.makeMac("HMAC(SHA-512)"); 147 } 148 catch(AlgorithmNotFound) {} 149 150 return null; 151 } 152 153 static if (BOTAN_TEST): 154 import botan.test; 155 import botan.rng.auto_rng; 156 157 static if (BOTAN_HAS_TESTS && !SKIP_PASSHASH9_TEST) unittest 158 { 159 import botan.libstate.libstate; 160 globalState(); 161 logDebug("Testing passhash9.d ..."); 162 size_t fails = 0; 163 164 const string input = "secret"; 165 const string fixed_hash = "$9$AAAKhiHXTIUhNhbegwBXJvk03XXJdzFMy+i3GFMIBYKtthTTmXZA"; 166 167 size_t ran = 0; 168 169 ++ran; 170 if (!checkPasshash9(input, fixed_hash)) 171 { 172 logTrace("Passhash9 fixed input test failed"); 173 fails++; 174 } 175 176 Unique!AutoSeededRNG rng = new AutoSeededRNG; 177 178 for(ubyte alg_id = 0; alg_id <= 4; ++alg_id) 179 { 180 string gen_hash = generatePasshash9(input, *rng, 2, alg_id); 181 182 ++ran; 183 if (!checkPasshash9(input, gen_hash)) 184 { 185 logTrace("Passhash9 gen and check " ~ alg_id.to!string ~ " failed"); 186 ++fails; 187 } 188 } 189 190 testReport("Passhash9", ran, fails); 191 }