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 }