1 module chloride.password;
2 
3 import chloride.core;
4 import chloride.random : randomArray;
5 
6 import std.array : uninitializedArray;
7 import std.algorithm.mutation : copy, fill;
8 import std.exception: assumeUnique;
9 import std..string : fromStringz, toStringz;
10 
11 import deimos.sodium.crypto_pwhash_scryptsalsa208sha256;
12 import deimos.sodium.crypto_pwhash;
13 
14 /**
15  * Algorithm to use for the password hashing.
16  */
17 enum Algorithm {
18   Scrypt,
19   Argon2
20 }
21 
22 /**
23  * Struct containing configuration for password hashing. Specifically the parameters
24  * to control the amount of CPU and memory required.
25  */
26 struct PwHashConfig {
27   ulong opslimit;
28   size_t memlimit;
29 }
30 
31 template PwHash(Algorithm alg) {
32   static if (alg == Algorithm.Scrypt) {
33     alias Salt = ubyte[crypto_pwhash_scryptsalsa208sha256_SALTBYTES];
34     alias PwStringBytes = crypto_pwhash_scryptsalsa208sha256_STRBYTES;
35 
36     /**
37      * Password hashing config suitable for interactive use.
38      */
39     enum interactivePwHashConfig = PwHashConfig(
40         crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE,
41         crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE);
42     /**
43      * Password hashing config suitable for highly sensitive data
44      */
45     enum sensitivePwHashConfig = PwHashConfig(
46         crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_SENSITIVE,
47         crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_SENSITIVE);
48   } else static if (alg == Algorithm.Argon2) {
49     alias Salt = ubyte[crypto_pwhash_SALTBYTES];
50     alias PwStringBytes = crypto_pwhash_STRBYTES;
51 
52     /**
53      * Password hashing config suitable for interactive use.
54      */
55     enum interactivePwHashConfig = PwHashConfig(
56         crypto_pwhash_OPSLIMIT_INTERACTIVE,
57         crypto_pwhash_MEMLIMIT_INTERACTIVE);
58     /**
59      * Password hashing config suitable for moderate use.
60      */
61     enum moderatePwHashConfig = PwHashConfig(
62         crypto_pwhash_OPSLIMIT_MODERATE,
63         crypto_pwhash_MEMLIMIT_MODERATE);
64 
65     /**
66      * Password hashing config suitable for highly sensitive data.
67      */
68     enum sensitivePwHashConfig = PwHashConfig(
69         crypto_pwhash_OPSLIMIT_SENSITIVE,
70         crypto_pwhash_MEMLIMIT_SENSITIVE);
71 
72   }
73 
74   void hashPasswordBuffer(ubyte[] out_, in char[] password, in Salt salt, PwHashConfig config) {
75     static if( alg == Algorithm.Scrypt ) {
76       int result = crypto_pwhash_scryptsalsa208sha256(
77           out_.ptr, out_.length,
78           password.ptr, password.length,
79           salt.ptr, config.opslimit, config.memlimit);
80     } else static if (alg == Algorithm.Argon2) {
81       int result = crypto_pwhash(
82           out_.ptr, out_.length,
83           password.ptr, password.length,
84           salt.ptr, config.opslimit, config.memlimit,
85           crypto_pwhash_ALG_DEFAULT);
86     }
87     enforceSodium(result == 0);
88   }
89 
90   /**
91    * Create a string that can be used to store a password safely.
92    * It includes the hash, the salt, and information about the CPU and
93    * memory limits used to compute it.
94    */
95   string hashPassword(string password, PwHashConfig config) {
96     char[PwStringBytes] out_ = void;
97     static if (alg == Algorithm.Scrypt) {
98       int result = crypto_pwhash_scryptsalsa208sha256_str(
99           out_, password.ptr, password.length,
100           config.opslimit, config.memlimit);
101     } else static if (alg == Algorithm.Argon2) {
102       int result = crypto_pwhash_str(
103           out_, password.ptr, password.length,
104           config.opslimit, config.memlimit);
105     }
106     enforceSodium(result == 0);
107     return fromStringz(out_.ptr).idup;
108   }
109 
110   /**
111    * Hash a password with a salt. Returns the hash as a `ubyte[]`.
112    */
113   ubyte[] hashPassword(in char[] password, in Salt salt, PwHashConfig config, size_t length) {
114       ubyte[] hash = uninitializedArray!(ubyte[])(length);
115       hashPasswordBuffer(hash, password, salt, config);
116       return hash;
117   }
118 
119   /**
120    * Verify a password against a storage string obtained from `hashPassword`
121    */
122   bool verifyPassword(string password, string storageString) {
123     char[PwStringBytes] data = '\0';
124     assert(storageString.length < data.length);
125     storageString.copy(data[]);
126     static if (alg == Algorithm.Scrypt) {
127       int result = crypto_pwhash_scryptsalsa208sha256_str_verify(data,
128           password.ptr, password.length);
129     } else static if (alg == Algorithm.Argon2) {
130       int result = crypto_pwhash_str_verify(data,
131           password.ptr, password.length);
132     }
133     return result == 0;
134   }
135 
136 
137   /**
138    * Verify a password with a hash and salt
139    */
140   bool verifyPassword(in ubyte[] hash, string password, in Salt salt, PwHashConfig config) {
141     import core.memory : GC;
142 
143     auto hashAttempt = uninitializedArray!(ubyte[])(hash.length);
144     scope(exit) {
145       GC.free(hashAttempt.ptr);
146     }
147     hashPasswordBuffer(hashAttempt, password, salt, config);
148     return hashAttempt == hash;
149   }
150 
151   /**
152    * Generate a salt suitable for hashing passwords.
153    */
154   alias makeSalt = randomArray!Salt;
155 
156   ///
157   unittest {
158     auto salt = makeSalt();
159     auto hash = hashPassword("password", salt, interactivePwHashConfig, 32);
160     assert(verifyPassword(hash, "password", salt, interactivePwHashConfig));
161   }
162 
163   ///
164   unittest {
165     import std.stdio;
166     auto storageString = hashPassword("password", interactivePwHashConfig);
167     writeln("Hashed Password: ", storageString);
168     assert(verifyPassword("password", storageString));
169   }
170 }
171 
172 alias Argon2 = PwHash!(Algorithm.Argon2);
173 alias Scrypt = PwHash!(Algorithm.Scrypt);
174 
175 
176 /**
177  * Convenience function to hash a password.
178  *
179  * If the algorithm isn't supplied use Argon2 as the default.
180  */
181 string hashPassword(Algorithm alg, string password, PwHashConfig config) {
182     final switch (alg) {
183         case Algorithm.Argon2:
184             return Argon2.hashPassword(password, config);
185         case Algorithm.Scrypt:
186             return Scrypt.hashPassword(password, config);
187     }
188 }
189 
190 /// ditto
191 string hashPassword(string password, PwHashConfig config) {
192     return Argon2.hashPassword(password, config);
193 }
194 
195 
196 /**
197  * Convenience function to verify a password.
198  */
199 bool verifyPassword(Algorithm alg, string password, string hash) {
200     final switch (alg) {
201         case Algorithm.Argon2:
202             return Argon2.verifyPassword(password, hash);
203         case Algorithm.Scrypt:
204             return Scrypt.verifyPassword(password, hash);
205     }
206 }
207 
208 /**
209  * Verify a passwod with a hash string.
210  *
211  * This will attempt to determine the correct algorithm from a prefix
212  * in the hash. If the prefix isn't known it will return false.
213  */
214 bool verifyPassword(string password, string hash) {
215     import std.algorithm.searching;
216     import std.conv;
217     import deimos.sodium.crypto_pwhash_argon2i;
218 
219     const ARGON_PREFIX = to!string(crypto_pwhash_argon2i_STRPREFIX);
220     const SCRYPT_PREFIX = to!string(crypto_pwhash_scryptsalsa208sha256_STRPREFIX);
221 
222     // figure out which algorithm was used
223     if (hash.startsWith(ARGON_PREFIX)) {
224         return Argon2.verifyPassword(password, hash);
225     } else if (hash.startsWith(SCRYPT_PREFIX)) {
226         return Scrypt.verifyPassword(password, hash);
227     } else {
228         return false;
229     }
230 }
231 
232 ///
233 unittest {
234     auto hash = Scrypt.hashPassword("password", Scrypt.interactivePwHashConfig);
235     assert(verifyPassword("password", hash));
236     assert(!verifyPassword("bad pass", hash));
237 }
238 
239 ///
240 unittest {
241     auto hash = Argon2.hashPassword("password", Argon2.interactivePwHashConfig);
242     assert(verifyPassword("password", hash));
243     assert(!verifyPassword("bad pass", hash));
244 }
245 
246 
247 /**
248  * An object that can hash and verify passwords.
249  */
250 interface PwHasher {
251     /// Hash a password
252     string hashPassword(string password) const;
253 
254     /// Verify a password matches a hash made with `hashPassword`
255     bool verifyPassword(string password, string hash) const;
256 }
257 
258 /**
259  * A `PwHasher` that uses the argon2 algorithm
260  */
261 class Argon2Hasher: PwHasher {
262     private PwHashConfig config;
263 
264     /**
265      * Params:
266      *   config = Configuration for hashing
267      */
268     this(PwHashConfig config) {
269         this.config = config;
270     }
271 
272     string hashPassword(string password) const {
273         return Argon2.hashPassword(password, config);
274     }
275 
276     bool verifyPassword(string password, string hash) const {
277         return Argon2.verifyPassword(password, hash);
278     }
279 }
280 
281 /**
282  * A `PwHasher` that uses the scrypt algorithm
283  */
284 class ScryptHasher: PwHasher {
285     private PwHashConfig config;
286 
287     /**
288      * Params:
289      *   config = Configuration for hashing
290      */
291     this(PwHashConfig config) {
292         this.config = config;
293     }
294 
295     string hashPassword(string password) const {
296         return Scrypt.hashPassword(password, config);
297     }
298 
299     bool verifyPassword(string password, string hash) const {
300         return Scrypt.verifyPassword(password, hash);
301     }
302 }