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 }