1 /++
2 DAuth - Salted Hashed Password Library for D
3 Core package
4 +/
5 module dauth.core;
6 
7 import std.algorithm;
8 import std.array;
9 import ascii = std.ascii;
10 import std.base64;
11 import std.conv;
12 import std.digest.crc;
13 import std.digest.md;
14 import std.digest.ripemd;
15 import std.digest.sha;
16 import std.exception;
17 import std.functional;
18 import std.random;
19 import std.range;
20 import std.traits;
21 import std.typecons;
22 
23 import dauth.random : randomSalt;
24 import dauth.hashdrbg;
25 
26 // Only use dauth.sha if SHA-2 isn't in Phobos (ie, DMD 2.065 and below)
27 static if(!is(std.digest.sha.SHA512))
28 {
29 	import dauth.sha;
30 
31 	private alias SHA1 = dauth.sha.SHA1;
32 	private alias SHA1Digest = dauth.sha.SHA1Digest;
33 	private alias sha1Of = dauth.sha.sha1Of;
34 }
35 
36 version(DAuth_Unittest)
37 {
38 	version(DAuth_Unittest_Quiet) {} else
39 		version = Loud_Unittest;
40 	
41 	version(Loud_Unittest)
42 		import std.stdio;
43 	
44 	void unitlog(string str)
45 	{
46 		version(Loud_Unittest)
47 		{
48 			writeln("unittest DAuth: ", str);
49 			stdout.flush();
50 		}
51 	}
52 }
53 
54 version(DAuth_AllowWeakSecurity) {} else
55 {
56 	version = DisallowWeakSecurity;
57 }
58 
59 alias Salt = ubyte[]; /// Salt type
60 alias Salter(TDigest) = void delegate(ref TDigest, Password, Salt); /// Convenience alias for salter delegates.
61 alias DefaultCryptoRand = HashDRBGStream!(SHA512, "DAuth"); /// Default is Hash_DRBG using SHA-512
62 alias DefaultDigest = SHA512; /// Default is SHA-512
63 alias DefaultDigestClass = WrapperDigest!DefaultDigest; /// OO-style version of 'DefaultDigest'.
64 alias TokenBase64 = Base64Impl!('-', '_', '~'); /// Implementation of Base64 engine used for tokens.
65 
66 /++
67 Default implementation of 'digestCodeOfObj' for DAuth-style hash strings.
68 See 'Hash!(TDigest).toString' for more info.
69 +/
70 string defaultDigestCodeOfObj(Digest digest)
71 {
72 	if     (cast( CRC32Digest      )digest) return "CRC32";
73 	else if(cast( MD5Digest        )digest) return "MD5";
74 	else if(cast( RIPEMD160Digest  )digest) return "RIPEMD160";
75 	else if(cast( SHA1Digest       )digest) return "SHA1";
76 	else if(cast( SHA224Digest     )digest) return "SHA224";
77 	else if(cast( SHA256Digest     )digest) return "SHA256";
78 	else if(cast( SHA384Digest     )digest) return "SHA384";
79 	else if(cast( SHA512Digest     )digest) return "SHA512";
80 	else if(cast( SHA512_224Digest )digest) return "SHA512_224";
81 	else if(cast( SHA512_256Digest )digest) return "SHA512_256";
82 	else
83 		throw new UnknownDigestException("Unknown digest type");
84 }
85 
86 /++
87 Default implementation of 'digestFromCode' for DAuth-style hash strings.
88 See 'parseHash' for more info.
89 +/
90 Digest defaultDigestFromCode(string digestCode)
91 {
92 	switch(digestCode)
93 	{
94 	case "CRC32":      return new CRC32Digest();
95 	case "MD5":        return new MD5Digest();
96 	case "RIPEMD160":  return new RIPEMD160Digest();
97 	case "SHA1":       return new SHA1Digest();
98 	case "SHA224":     return new SHA224Digest();
99 	case "SHA256":     return new SHA256Digest();
100 	case "SHA384":     return new SHA384Digest();
101 	case "SHA512":     return new SHA512Digest();
102 	case "SHA512_224": return new SHA512_224Digest();
103 	case "SHA512_256": return new SHA512_256Digest();
104 	default:
105 		throw new UnknownDigestException("Unknown digest code");
106 	}
107 }
108 
109 /++
110 Default implementation of 'digestCodeOfObj' for Unix crypt-style hash strings.
111 See 'Hash!(TDigest).toString' for more info.
112 +/
113 string defaultDigestCryptCodeOfObj(Digest digest)
114 {
115 	if     (cast( MD5Digest    )digest) return "1";
116 	else if(cast( SHA256Digest )digest) return "5";
117 	else if(cast( SHA512Digest )digest) return "6";
118 	else
119 		throw new UnknownDigestException("Unknown digest type");
120 }
121 
122 /++
123 Default implementation of 'digestFromCode' for Unix crypt-style hash strings.
124 See 'parseHash' for more info.
125 +/
126 Digest defaultDigestFromCryptCode(string digestCode)
127 {
128 	switch(digestCode)
129 	{
130 	case "":   throw new UnknownDigestException(`Old crypt-DES not currently supported`);
131 	case "1":  return new MD5Digest();
132 	case "5":  return new SHA256Digest();
133 	case "6":  return new SHA512Digest();
134 	default:
135 		throw new UnknownDigestException("Unknown digest code");
136 	}
137 }
138 
139 /// Default salter for 'makeHash' and 'isSameHash'.
140 void defaultSalter(TDigest)(ref TDigest digest, Password password, Salt salt)
141 	if(isAnyDigest!TDigest)
142 {
143 	digest.put(cast(immutable(ubyte)[])salt);
144 	digest.put(password.data);
145 }
146 
147 /++
148 Note, this only checks Phobos's RNG's and digests, and only by type. This
149 works on a blacklist basis - it blindly accepts any Phobos-compatible RNG
150 or digest it does not know about. This is only supplied as a convenience. It
151 is always your own responsibility to select an appropriate algorithm for your
152 own needs.
153 
154 And yes, unfortunately, this does currently rule out all RNG's and digests
155 currently in Phobos (as of v2.065). They are all known to be fairly weak
156 for password-hashing purposes, even SHA1 which despite being heavily used
157 has known security flaws.
158 
159 For random number generators, you should use a CPRNG (cryptographically secure
160 pseudorandom number generator):
161     $(LINK http://en.wikipedia.org/wiki/Cryptographically_secure_pseudo-random_number_generator )
162 
163 For digests, you should use one of the SHA-2 algorithms (for example, SHA512)
164 or, better yet, an established "key stretching" algorithm
165 ( $(LINK http://en.wikipedia.org/wiki/Key_stretching#History) ), intended
166 for password hashing. These contain deliberate inefficiencies that cannot be
167 optimized away even with massive parallelization (such as a GPU cluster). These
168 are NOT too inefficient to use for even high-traffic authentication, but they
169 do thwart the parallelized brute force attacks that algorithms used for
170 streaming data encryption, such as SHA, are increasingly susceptible to.
171     $(LINK https://crackstation.net/hashing-security.htm)
172 +/
173 bool isKnownWeak(T)() if(isDigest!T || isSomeRandom!T)
174 {
175 	return
176 		is(T == CRC32) ||
177 		is(T == MD5) ||
178 		is(T == RIPEMD160) ||
179 		is(T == SHA1) ||
180 		
181 		// Requires to-be-released DMD 2.066:
182 		//__traits(isSame, TemplateOf!T, LinearCongruentialEngine) ||
183 		//__traits(isSame, TemplateOf!T, MersenneTwisterEngine) ||
184 		//__traits(isSame, TemplateOf!T, XorshiftEngine);
185 		is(T == MinstdRand0) ||
186 		is(T == MinstdRand) ||
187 		is(T == Mt19937) ||
188 		is(T == Xorshift32) ||
189 		is(T == Xorshift64) ||
190 		is(T == Xorshift96) ||
191 		is(T == Xorshift128) ||
192 		is(T == Xorshift160) ||
193 		is(T == Xorshift192) ||
194 		is(T == Xorshift);
195 }
196 
197 ///ditto
198 bool isKnownWeak(T)(T digest) if(is(T : Digest))
199 {
200 	return
201 		cast(CRC32Digest)digest ||
202 		cast(MD5Digest)digest ||
203 		cast(RIPEMD160Digest)digest ||
204 		cast(SHA1Digest)digest;
205 }
206 
207 private void validateStrength(T)() if(isDigest!T || isSomeRandom!T)
208 {
209 	version(DisallowWeakSecurity)
210 	{
211 		static if(isKnownWeak!T())
212 		{
213 			pragma(msg, "ERROR: "~T.stringof~" - "~KnownWeakException.message);
214 			static assert(false);
215 		}
216 	}
217 }
218 
219 private void validateStrength(Digest digest)
220 {
221 	version(DisallowWeakSecurity)
222 	{
223 		enforce(!isKnownWeak(digest),
224 			new KnownWeakException(defaultDigestCodeOfObj(digest)));
225 	}
226 }
227 
228 /++
229 Thrown whenever a digest type cannot be determined.
230 For example, when the provided (or default) 'digestCodeOfObj' or 'digestFromCode'
231 delegates fail to find a match. Or when passing isSameHash a
232 Hash!Digest with a null 'digest' member (which prevents it from determining
233 the correct digest to match with).
234 +/
235 class UnknownDigestException : Exception
236 {
237 	this(string msg) { super(msg); }
238 }
239 
240 /++
241 Thrown when a known-weak algortihm or setting it attempted, UNLESS
242 compiled with '-version=DAuth_AllowWeakSecurity'
243 +/
244 class KnownWeakException : Exception
245 {
246 	static enum message =
247 		"This is known to be weak for salted password hashing. "~
248 		"If you understand and accept the risks, you can force DAuth "~
249 		"to allow it with -version=DAuth_AllowWeakSecurity";
250 	
251 	this(string algoName)
252 	{
253 		super(algoName ~ " - " ~ message);
254 	}
255 }
256 
257 /++
258 Like std.digest.digest.isDigest, but also accepts OO-style digests
259 (ie. classes deriving from interface std.digest.digest.Digest)
260 +/
261 template isAnyDigest(TDigest)
262 {
263 	enum isAnyDigest =
264 		isDigest!TDigest ||
265 		is(TDigest : Digest);
266 }
267 
268 version(DAuth_Unittest)
269 unittest
270 {
271 	struct Foo {}
272 	static assert(isAnyDigest!SHA1);
273 	static assert(isAnyDigest!SHA1Digest);
274 	static assert(isAnyDigest!SHA256);
275 	static assert(isAnyDigest!SHA256Digest);
276 	static assert(!isAnyDigest!Foo);
277 	static assert(!isAnyDigest!Object);
278 }
279 
280 /++
281 Like std.digest.digest.DigestType, but also accepts OO-style digests
282 (ie. classes deriving from interface std.digest.digest.Digest)
283 +/
284 template AnyDigestType(TDigest)
285 {
286 	static assert(isAnyDigest!TDigest,
287 		TDigest.stringof ~ " is not a template-style or OO-style digest (fails isAnyDigest!T)");
288 	
289 	static if(isDigest!TDigest)
290 		alias AnyDigestType = DigestType!TDigest;
291 	else
292 		alias AnyDigestType = ubyte[];
293 }
294 
295 version(DAuth_Unittest)
296 unittest
297 {
298 	struct Foo {}
299 	static assert( is(AnyDigestType!SHA1 == ubyte[20]) );
300 	static assert( is(AnyDigestType!SHA1Digest == ubyte[]) );
301 	static assert( is(AnyDigestType!SHA512 == ubyte[64]) );
302 	static assert( is(AnyDigestType!SHA512Digest == ubyte[]) );
303 	static assert( !is(AnyDigestType!Foo) );
304 	static assert( !is(AnyDigestType!Object) );
305 }
306 
307 /// Tests if the type is an instance of struct Hash(some digest)
308 template isHash(T)
309 {
310 	enum isHash = is( Hash!(TemplateArgsOf!(T)[0]) == T );
311 }
312 
313 version(DAuth_Unittest)
314 unittest
315 {
316 	struct Foo {}
317 	struct Bar(T) { T digest; }
318 
319 	static assert( isHash!(Hash!SHA1) );
320 	static assert( isHash!(Hash!SHA1Digest) );
321 	static assert( isHash!(Hash!SHA512) );
322 	static assert( isHash!(Hash!SHA512Digest) );
323 
324 	static assert( !isHash!Foo              );
325 	static assert( !isHash!(Bar!int)        );
326 	static assert( !isHash!(Bar!Object)     );
327 	static assert( !isHash!(Bar!SHA1)       );
328 	static assert( !isHash!(Bar!SHA1Digest) );
329 }
330 
331 /// Retreive the digest type of a struct Hash(some digest)
332 template DigestOf(T) if(isHash!T)
333 {
334 	alias DigestOf = TemplateArgsOf!(T)[0];
335 }
336 
337 version(DAuth_Unittest)
338 unittest
339 {
340 	static assert(is( DigestOf!(Hash!SHA1  ) == SHA1  ));
341 	static assert(is( DigestOf!(Hash!SHA512) == SHA512));
342 	static assert(is( DigestOf!(Hash!Digest) == Digest));
343 }
344 
345 string getDigestCode(TDigest)(string delegate(Digest) digestCodeOfObj, TDigest digest)
346 	if(isAnyDigest!TDigest)
347 {
348 	static if(is(TDigest : Digest))
349 		return digestCodeOfObj(digest);
350 	else
351 	{
352 		auto digestObj = new WrapperDigest!TDigest();
353 		return digestCodeOfObj(digestObj);
354 	}
355 }
356 
357 /++
358 A reference-counted type for passwords. The memory containing the password
359 is automatically zeroed-out when there are no more references or when
360 a new password is assigned.
361 
362 If you keep any direct references to Password.data, be aware it may get cleared.
363 
364 Create a Password via functions 'toPassword' or 'dupPassword'.
365 
366 The payload is a private struct that supports the following:
367 
368 ---------------------------------------------------------
369 	@property ubyte[] data(): Retrieve the actual plaintext password
370 
371 	@property size_t length() const: Retrieve the password length
372 
373 	void opAssign(PasswordData rhs): Assignment
374 
375 	void opAssign(ubyte[] rhs): Assignment
376 
377 	~this(): Destructor
378 ---------------------------------------------------------
379 +/
380 alias Password = RefCounted!PasswordData;
381 
382 /// Payload of Password
383 private struct PasswordData
384 {
385 	private ubyte[] _data;
386 	
387 	@property ubyte[] data()
388 	{
389 		return _data;
390 	}
391 	
392 	@property size_t length() const
393 	{
394 		return _data.length;
395 	}
396 	
397 	void opAssign(PasswordData rhs)
398 	{
399 		opAssign(rhs._data);
400 	}
401 	
402 	void opAssign(ubyte[] rhs)
403 	{
404 		clear();
405 		this._data = rhs;
406 	}
407 	
408 	~this()
409 	{
410 		clear();
411 	}
412 
413 	private void clear()
414 	{
415 		_data[] = 0;
416 	}
417 }
418 
419 /++
420 Constructs a Password from a ubyte[].
421 Mainly provided for syntactic consistency with 'toPassword(char[])'.
422 +/
423 Password toPassword(ubyte[] password)
424 {
425 	return Password(password);
426 }
427 
428 /++
429 Constructs a Password from a char[] so you don't have to cast to ubyte[],
430 and don't accidentally cast away immutability.
431 +/
432 Password toPassword(char[] password)
433 {
434 	return Password(cast(ubyte[])password);
435 }
436 
437 /++
438 This function exists as a convenience in case you need it, HOWEVER it's
439 recommended to design your code so you DON'T need to use this (use
440 toPassword instead):
441 
442 Using this to create a Password cannot protect the in-memory data of your
443 original string because a string's data is immutable (this function must
444 .dup the memory).
445 
446 While immutability usually improves safety, you should avoid ever storing
447 unhashed passwords in immutables because they cannot be reliably
448 zero-ed out.
449 +/
450 Password dupPassword(string password)
451 {
452 	return toPassword(password.dup);
453 }
454 
455 /++
456 Contains all the relevant information for a salted hash.
457 Note the digest type can be obtained via DigestOf!(SomeHashType).
458 +/
459 struct Hash(TDigest) if(isAnyDigest!TDigest)
460 {
461 	Salt salt; /// The salt that was used.
462 	
463 	/++
464 	The hash of the salted password. To obtain a printable DB-friendly
465 	string, pass this to std.digest.digest.toHexString.
466 	+/
467 	AnyDigestType!TDigest hash;
468 	
469 	/// The digest that was used for hashing.
470 	TDigest digest;
471 	
472 	/++
473 	Encodes the digest, salt and hash into a convenient forward-compatible
474 	string format, ready for insertion into a database.
475 	
476 	To support additional digests besides the built-in (Phobos's CRC32, MD5,
477 	RIPEMD160 and SHA), supply a custom delegate for digestCodeOfObj.
478 	Your custom digestCodeOfObj only needs to handle OO-style digests.
479 	As long as the OO-style digests were created using Phobos's
480 	WrapperDigest template, the template-style version will be handled
481 	automatically. You can defer to DAuth's defaultDigestCodeOfObj to
482 	handle the built-in digests.
483 	
484 	Example:
485 	-------------------
486 	import std.digest.digest;
487 	import dauth;
488 	
489 	struct BBQ42 {...}
490 	static assert(isDigest!BBQ42);
491 	alias BBQ42Digest = WrapperDigest!BBQ42;
492 	
493 	string customDigestCodeOfObj(Digest digest)
494 	{
495 	    if     (cast(BBQ42Digest)digest) return "BBQ42";
496 	    else if(cast(FAQ17Digest)digest) return "FAQ17";
497 	    else
498 	        return defaultDigestCodeOfObj(digest);
499 	}
500 	
501 	void doStuff(Hash!BBQ42 hash)
502 	{
503 	    writeln( hash.toString(&customDigestCodeOfObj) );
504 	}
505 	-------------------
506 	
507 	Optional_Params:
508 	digestCodeOfObj - Default value is 'toDelegate(&defaultDigestCodeOfObj)'
509 	+/
510 	string toString(string delegate(Digest) digestCodeOfObj = toDelegate(&defaultDigestCodeOfObj))
511 	{
512 		Appender!string sink;
513 		toString(sink, digestCodeOfObj);
514 		return sink.data;
515 	}
516 
517 	///ditto
518 	void toString(Sink)(ref Sink sink,
519 		string delegate(Digest) digestCodeOfObj = toDelegate(&defaultDigestCodeOfObj))
520 		if(isOutputRange!(Sink, const(char)))
521 	{
522 		sink.put('[');
523 		sink.put(getDigestCode(digestCodeOfObj, digest));
524 		sink.put(']');
525 		Base64.encode(salt, sink);
526 		sink.put('$');
527 		Base64.encode(hash, sink);
528 	}
529 
530 	/++
531 	Just like toString, but instead of standard DAuth-style format, the
532 	output string is in the crypt(3)-style format.
533 	
534 	The crypt(3) format does not support all hash types, and DAuth doesn't
535 	necessarily support all possible forms of crypt(3) hashes (although it
536 	does strive to support as many as possible).
537 	
538 	DAuth currently supports crypt(3)-style format for MD5, SHA256 and
539 	SHA512 hashes. Other hashes (unless manually handled by a custom
540 	digestCodeOfObj) will cause an UnknownDigestException to be thrown.
541 	
542 	The default digestCodeOfObj for this function is defaultDigestCryptCodeOfObj.
543 	
544 	Optional_Params:
545 	digestCodeOfObj - Default value is 'toDelegate(&defaultDigestCryptCodeOfObj)'
546 
547 	See also: $(LINK https://en.wikipedia.org/wiki/Crypt_%28C%29)
548 	+/
549 	string toCryptString(string delegate(Digest) digestCodeOfObj = toDelegate(&defaultDigestCryptCodeOfObj))
550 	{
551 		Appender!string sink;
552 		toCryptString(sink, digestCodeOfObj);
553 		return sink.data;
554 	}
555 
556 	///ditto
557 	void toCryptString(Sink)(ref Sink sink,
558 		string delegate(Digest) digestCodeOfObj = toDelegate(&defaultDigestCryptCodeOfObj))
559 		if(isOutputRange!(Sink, const(char)))
560 	{
561 		sink.put('$');
562 		sink.put(getDigestCode(digestCodeOfObj, digest));
563 		sink.put('$');
564 		Base64.encode(salt, sink);
565 		sink.put('$');
566 		Base64.encode(hash, sink);
567 	}
568 }
569 
570 /++
571 Generates a salted password using any Phobos-compatible digest, default being SHA-512.
572 
573 (Note: An established "key stretching" algorithm
574 ( $(LINK http://en.wikipedia.org/wiki/Key_stretching#History) ) would be an even
575 better choice of digest since they provide better protection against
576 highly-parallelized (ex: GPU) brute-force attacks. But SHA-512, as an SHA-2
577 algorithm, is still considered cryptographically secure.)
578 
579 Supports both template-style and OO-style digests. See the documentation of
580 std.digest.digest for details.
581 
582 Salt is optional. It will be generated at random if not provided.
583 
584 Normally, the salt and password are combined as (psuedocode) 'salt~password'.
585 There is no cryptographic benefit to combining the salt and password any
586 other way. However, if you need to support an alternate method for
587 compatibility purposes, you can do so by providing a custom salter delegate.
588 See the implementation of DAuth's defaultSalter to see how to do this.
589 
590 If using an OO-style Digest, then digest MUST be non-null. Otherwise,
591 an UnknownDigestException will be thrown.
592 
593 Optional_Params:
594 salt - Default value is 'randomSalt()'
595 
596 salter - Default value is 'toDelegate(&defaultSalter!TDigest)'
597 +/
598 Hash!TDigest makeHash(TDigest = DefaultDigest)
599 	(Password password, Salt salt = randomSalt(), Salter!TDigest salter = toDelegate(&defaultSalter!TDigest))
600 	if(isDigest!TDigest)
601 {
602 	validateStrength!TDigest();
603 	TDigest digest;
604 	return makeHashImpl!TDigest(digest, password, salt, salter);
605 }
606 
607 ///ditto
608 Hash!TDigest makeHash(TDigest = DefaultDigest)(Password password, Salter!TDigest salter)
609 	if(isDigest!TDigest)
610 {
611 	validateStrength!TDigest();
612 	TDigest digest;
613 	return makeHashImpl(digest, password, randomSalt(), salter);
614 }
615 
616 ///ditto
617 Hash!Digest makeHash()(Digest digest, Password password, Salt salt = randomSalt(),
618 	Salter!Digest salter = toDelegate(&defaultSalter!Digest))
619 {
620 	enforce(digest, new UnknownDigestException("digest was null, don't know what digest to use"));
621 	validateStrength(digest);
622 	return makeHashImpl!Digest(digest, password, salt, salter);
623 }
624 
625 ///ditto
626 Hash!Digest makeHash()(Digest digest, Password password, Salter!Digest salter)
627 {
628 	enforce(digest, new UnknownDigestException("digest was null, don't know what digest to use"));
629 	validateStrength(digest);
630 	return makeHashImpl!Digest(digest, password, randomSalt(), salter);
631 }
632 
633 private Hash!TDigest makeHashImpl(TDigest)
634 	(ref TDigest digest, Password password, Salt salt, Salter!TDigest salter)
635 	if(isAnyDigest!TDigest)
636 {
637 	Hash!TDigest ret;
638 	ret.digest = digest;
639 	ret.salt   = salt;
640 	
641 	static if(isDigest!TDigest) // template-based digest
642 		ret.digest.start();
643 	else
644 		ret.digest.reset(); // OO-based digest
645 	
646 	salter(ret.digest, password, salt);
647 	ret.hash = ret.digest.finish();
648 	
649 	return ret;
650 }
651 
652 /++
653 Parses a string that was encoded by Hash.toString.
654 
655 Only OO-style digests are used since the digest is specified in the string
656 and therefore only known at runtime.
657 
658 Throws ConvException if the string is malformed.
659 
660 To support additional digests besides the built-in (Phobos's CRC32, MD5,
661 RIPEMD160 and SHA), supply a custom delegate for digestFromDAuthCode.
662 You can defer to DAuth's defaultDigestFromCode to handle the
663 built-in digests.
664 
665 Similarly, to extend crypt(3)-style to support additional digests beyond
666 DAuth's crypt(3) support, supply a custom delegate for digestFromCryptCode.
667 The default implementation is defaultDigestFromCryptCode.
668 
669 Example:
670 -------------------
671 import std.digest.digest;
672 import dauth;
673 
674 struct BBQ42 {...}
675 static assert(isDigest!BBQ42);
676 alias BBQ42Digest = WrapperDigest!BBQ42;
677 
678 Digest customDigestFromCode(string digestCode)
679 {
680     switch(digestCode)
681     {
682     case "BBQ42": return new BBQ42Digest();
683     case "FAQ17": return new FAQ17Digest();
684     default:
685         return defaultDigestFromCode(digestCode);
686     }
687 }
688 
689 void doStuff(string hashString)
690 {
691     auto hash = parseHash(hashString, &customDigestFromCode);
692 }
693 -------------------
694 
695 Optional_Params:
696 digestFromDAuthCode - Default value is 'toDelegate(&defaultDigestFromCode)'
697 
698 digestFromCryptCode - Default value is 'toDelegate(&defaultDigestFromCryptCode)'
699 +/
700 Hash!Digest parseHash(string str,
701 	Digest delegate(string) digestFromDAuthCode = toDelegate(&defaultDigestFromCode),
702 	Digest delegate(string) digestFromCryptCode = toDelegate(&defaultDigestFromCryptCode))
703 {
704 	enforceEx!ConvException(!str.empty);
705 	if(str[0] == '[')
706 		return parseDAuthHash(str, digestFromDAuthCode);
707 	else if(str[0] == '$' || str.length == 13)
708 		return parseCryptHash(str, digestFromCryptCode);
709 	
710 	throw new ConvException("Hash string is neither valid DAuth-style nor crypt-style");
711 }
712 
713 ///ditto
714 Hash!Digest parseDAuthHash(string str,
715 	Digest delegate(string) digestFromDAuthCode = toDelegate(&defaultDigestFromCode))
716 {
717 	// No need to mess with UTF
718 	auto bytes = cast(immutable(ubyte)[]) str;
719 	
720 	// Parse '['
721 	enforceEx!ConvException(!bytes.empty);
722 	enforceEx!ConvException(bytes.front == cast(ubyte)'[');
723 	bytes.popFront();
724 
725 	// Parse digest code
726 	auto splitRBracket = bytes.findSplit([']']);
727 	enforceEx!ConvException( !splitRBracket[0].empty && !splitRBracket[1].empty && !splitRBracket[2].empty );
728 	auto digestCode = splitRBracket[0];
729 	bytes = splitRBracket[2];
730 	
731 	// Split salt and hash
732 	auto splitDollar = bytes.findSplit(['$']);
733 	enforceEx!ConvException( !splitDollar[0].empty && !splitDollar[1].empty && !splitDollar[2].empty );
734 	auto salt = splitDollar[0];
735 	auto hash = splitDollar[2];
736 	
737 	// Construct Hash
738 	Hash!Digest result;
739 	result.salt   = Base64.decode(salt);
740 	result.hash   = Base64.decode(hash);
741 	result.digest = digestFromDAuthCode(cast(string)digestCode);
742 	
743 	return result;
744 }
745 
746 ///ditto
747 Hash!Digest parseCryptHash(string str,
748 	Digest delegate(string) digestFromCryptCode = toDelegate(&defaultDigestFromCryptCode))
749 {
750 	// No need to mess with UTF
751 	auto bytes = cast(immutable(ubyte)[]) str;
752 
753 	enforceEx!ConvException(!bytes.empty);
754 	
755 	// Old crypt-DES style?
756 	if(bytes[0] != cast(ubyte)'$' && bytes.length == 13)
757 	{
758 		auto salt = bytes[0..2];
759 		auto hash = bytes[2..$];
760 
761 		// Construct Hash
762 		Hash!Digest result;
763 		result.salt   = salt.dup;
764 		result.hash   = hash.dup;
765 		result.digest = digestFromCryptCode(null);
766 		
767 		return result;
768 	}
769 	
770 	// Parse initial '$'
771 	enforceEx!ConvException(bytes.front == cast(ubyte)'$');
772 	bytes.popFront();
773 	
774 	// Split digest code, salt and hash
775 	auto parts = bytes.splitter('$').array();
776 	enforceEx!ConvException(parts.length == 3);
777 	auto digestCode = parts[0];
778 	auto salt       = parts[1];
779 	auto hash       = parts[2];
780 	
781 	// Construct Hash
782 	Hash!Digest result;
783 	result.salt   = Base64.decode(salt);
784 	result.hash   = Base64.decode(hash);
785 	result.digest = digestFromCryptCode(cast(string)digestCode);
786 	
787 	return result;
788 }
789 
790 /++
791 Validates a password against an existing salted hash.
792 
793 If sHash is a Hash!Digest, then sHash.digest MUST be non-null. Otherwise
794 this function will have no other way to determine what digest to match
795 against, and an UnknownDigestException will be thrown.
796 
797 Optional_Params:
798 salter - Default value is 'toDelegate(&defaultSalter!TDigest)'
799 
800 digest - Default value is 'new DefaultDigestClass()'
801 +/
802 bool isSameHash(TDigest = DefaultDigest)(Password password, Hash!TDigest sHash,
803 	Salter!TDigest salter = toDelegate(&defaultSalter!TDigest))
804 	if(isDigest!TDigest)
805 {
806 	auto testHash = makeHash!TDigest(password, sHash.salt, salter);
807 	return lengthConstantEquals(testHash.hash, sHash.hash);
808 }
809 
810 ///ditto
811 bool isSameHash(TDigest = Digest)(Password password, Hash!TDigest sHash,
812 	Salter!Digest salter = toDelegate(&defaultSalter!Digest))
813 	if(is(TDigest : Digest))
814 {
815 	Hash!Digest testHash;
816 
817 	if(sHash.digest)
818 		testHash = makeHash(sHash.digest, password, sHash.salt, salter);
819 	else
820 	{
821 		static if(is(TDigest == Digest))
822 			throw new UnknownDigestException("Cannot determine digest from a Hash!Digest with a null 'digest' member.");
823 		else
824 			testHash = makeHash(new TDigest(), password, sHash.salt, salter);
825 	}
826 
827 	return lengthConstantEquals(testHash.hash, sHash.hash);
828 }
829 
830 ///ditto
831 bool isSameHash(TDigest = DefaultDigest)
832 	(Password password, DigestType!TDigest hash, Salt salt,
833 		Salter!TDigest salter = toDelegate(&defaultSalter!TDigest))
834 	if(isDigest!TDigest)
835 {
836 	auto testHash = makeHash!TDigest(password, salt, salter);
837 	return lengthConstantEquals(testHash.hash, hash);
838 }
839 
840 ///ditto
841 bool isSameHash()(Password password,
842 	ubyte[] hash, Salt salt, Digest digest = new DefaultDigestClass(),
843 	Salter!Digest salter = toDelegate(&defaultSalter!Digest))
844 {
845 	auto testHash = makeHash(digest, password, salt, salter);
846 	return lengthConstantEquals(testHash.hash, hash);
847 }
848 
849 ///ditto
850 bool isSameHash()(Password password,
851 	ubyte[] hash, Salt salt, Salter!Digest salter)
852 {
853 	auto testHash = makeHash(new DefaultDigestClass(), password, salt, salter);
854 	return lengthConstantEquals(testHash.hash, hash);
855 }
856 
857 /++
858 Alias for backwards compatibility.
859 
860 isPasswordCorrect will become deprecated in a future version. Use isSameHash instead.
861 +/
862 alias isPasswordCorrect = isSameHash;
863 
864 version(DAuth_Unittest)
865 unittest
866 {
867 	// For validity of sanity checks, these sha/md5 and base64 strings
868 	// were NOT generated using Phobos.
869 	auto plainText1        = dupPassword("hello world");
870 	enum md5Hash1          = cast(ubyte[16]) x"5eb63bbbe01eeed093cb22bb8f5acdc3";
871 	enum md5Hash1Base64    = "XrY7u+Ae7tCTyyK7j1rNww==";
872 	enum sha1Hash1         = cast(ubyte[20]) x"2aae6c35c94fcfb415dbe95f408b9ce91ee846ed";
873 	enum sha1Hash1Base64   = "Kq5sNclPz7QV2+lfQIuc6R7oRu0=";
874 	enum sha512Hash1       = cast(ubyte[64]) x"309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f";
875 	enum sha512Hash1Base64 = "MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==";
876 
877 	auto plainText2        = dupPassword("some salt");
878 	enum md5Hash2          = cast(ubyte[16]) x"befbc24b5c6a74591c0d8e6397b8a398";
879 	enum md5Hash2Base64    = "vvvCS1xqdFkcDY5jl7ijmA==";
880 	enum sha1Hash2         = cast(ubyte[20]) x"78bc8b0e186b0aa698f12dc27736b492e4dacfc8";
881 	enum sha1Hash2Base64   = "eLyLDhhrCqaY8S3Cdza0kuTaz8g=";
882 	enum sha512Hash2       = cast(ubyte[64]) x"637246608760dc79f00d3ad4fd26c246bb217e10f811cdbf6fe602c3981e98b8cadacadc452808ae393ac46e8a7e967aa99711d7fd7ed6c055264787f8043693";
883 	enum sha512Hash2Base64 = "Y3JGYIdg3HnwDTrU/SbCRrshfhD4Ec2/b+YCw5gemLjK2srcRSgIrjk6xG6KfpZ6qZcR1/1+1sBVJkeH+AQ2kw==";
884 	
885 	unitlog("Sanity checking unittest's data");
886 	assert(md5Of(plainText1.data) == md5Hash1);
887 	assert(md5Of(plainText2.data) == md5Hash2);
888 	assert(sha1Of(plainText1.data) == sha1Hash1);
889 	assert(sha1Of(plainText2.data) == sha1Hash2);
890 	assert(sha512Of(plainText1.data) == sha512Hash1);
891 	assert(sha512Of(plainText2.data) == sha512Hash2);
892 	assert(Base64.encode(md5Hash1) == md5Hash1Base64);
893 	assert(Base64.encode(md5Hash2) == md5Hash2Base64);
894 	assert(Base64.encode(sha1Hash1) == sha1Hash1Base64);
895 	assert(Base64.encode(sha1Hash2) == sha1Hash2Base64);
896 	assert(Base64.encode(sha512Hash1) == sha512Hash1Base64);
897 	assert(Base64.encode(sha512Hash2) == sha512Hash2Base64);
898 
899 	unitlog("Testing Hash.toString");
900 	Hash!SHA1 result1;
901 	result1.hash = cast(AnyDigestType!SHA1) sha1Hash1;
902 	result1.salt = cast(Salt)               sha1Hash2;
903 	assert( result1.toString() == text("[SHA1]", sha1Hash2Base64, "$", sha1Hash1Base64) );
904 
905 	Hash!MD5 result1_md5;
906 	result1_md5.hash = cast(AnyDigestType!MD5) md5Hash1;
907 	result1_md5.salt = cast(Salt)              md5Hash2;
908 	assert( result1_md5.toString() == text("[MD5]", md5Hash2Base64, "$", md5Hash1Base64) );
909 
910 	Hash!SHA512 result1_512;
911 	result1_512.hash = cast(AnyDigestType!SHA512) sha512Hash1;
912 	result1_512.salt = cast(Salt)                 sha512Hash2;
913 	assert( result1_512.toString() == text("[SHA512]", sha512Hash2Base64, "$", sha512Hash1Base64) );
914 	
915 	unitlog("Testing Hash.toString - crypt(3)");
916 	assertThrown!UnknownDigestException( result1.toCryptString() );
917 	assert( result1_md5.toCryptString() == text("$1$", md5Hash2Base64,    "$", md5Hash1Base64) );
918 	assert( result1_512.toCryptString() == text("$6$", sha512Hash2Base64, "$", sha512Hash1Base64) );
919 
920 	unitlog("Testing makeHash([digest,] pass, salt [, salter])");
921 	void altSalter(TDigest)(ref TDigest digest, Password password, Salt salt)
922 	{
923 		// Reverse order
924 		digest.put(password.data);
925 		digest.put(cast(immutable(ubyte)[])salt);
926 	}
927 	
928 	auto result2          = makeHash!SHA1(plainText1, cast(Salt)sha1Hash2[]);
929 	auto result2AltSalter = makeHash!SHA1(plainText1, cast(Salt)sha1Hash2[], &altSalter!SHA1);
930 	auto result3          = makeHash(new SHA1Digest(), plainText1, cast(Salt)sha1Hash2[]);
931 	auto result3AltSalter = makeHash(new SHA1Digest(), plainText1, cast(Salt)sha1Hash2[], &altSalter!Digest);
932 
933 	assert(result2.salt       == result3.salt);
934 	assert(result2.hash       == result3.hash);
935 	assert(result2.toString() == result3.toString());
936 	assert(result2.toString() == makeHash!SHA1(plainText1, cast(Salt)sha1Hash2[]).toString());
937 	assert(result2.salt == result1.salt);
938 
939 	assert(result2AltSalter.salt       == result3AltSalter.salt);
940 	assert(result2AltSalter.hash       == result3AltSalter.hash);
941 	assert(result2AltSalter.toString() == result3AltSalter.toString());
942 	assert(result2AltSalter.toString() == makeHash!SHA1(plainText1, cast(Salt)sha1Hash2[], &altSalter!SHA1).toString());
943 	
944 	assert(result2.salt       == result2AltSalter.salt);
945 	assert(result2.hash       != result2AltSalter.hash);
946 	assert(result2.toString() != result2AltSalter.toString());
947 
948 	auto result2_512          = makeHash!SHA512(plainText1, cast(Salt)sha512Hash2[]);
949 	auto result2_512AltSalter = makeHash!SHA512(plainText1, cast(Salt)sha512Hash2[], &altSalter!SHA512);
950 	auto result3_512          = makeHash(new SHA512Digest(), plainText1, cast(Salt)sha512Hash2[]);
951 	auto result3_512AltSalter = makeHash(new SHA512Digest(), plainText1, cast(Salt)sha512Hash2[], &altSalter!Digest);
952 
953 	assert(result2_512.salt       == result3_512.salt);
954 	assert(result2_512.hash       == result3_512.hash);
955 	assert(result2_512.toString() == result3_512.toString());
956 	assert(result2_512.toString() == makeHash!SHA512(plainText1, cast(Salt)sha512Hash2[]).toString());
957 	assert(result2_512.salt == result1_512.salt);
958 
959 	assert(result2_512AltSalter.salt       == result3_512AltSalter.salt);
960 	assert(result2_512AltSalter.hash       == result3_512AltSalter.hash);
961 	assert(result2_512AltSalter.toString() == result3_512AltSalter.toString());
962 	assert(result2_512AltSalter.toString() == makeHash!SHA512(plainText1, cast(Salt)sha512Hash2[], &altSalter!SHA512).toString());
963 	
964 	assert(result2_512.salt       == result2_512AltSalter.salt);
965 	assert(result2_512.hash       != result2_512AltSalter.hash);
966 	assert(result2_512.toString() != result2_512AltSalter.toString());
967 	
968 	assertThrown!UnknownDigestException( makeHash(cast(SHA1Digest)null, plainText1, cast(Salt)sha1Hash2[]) );
969 	assertThrown!UnknownDigestException( makeHash(cast(Digest)null,     plainText1, cast(Salt)sha1Hash2[]) );
970 
971 	unitlog("Testing makeHash(pass)");
972 	import dauth.random : randomPassword;
973 	auto resultRand1 = makeHash!SHA1(randomPassword());
974 	auto resultRand2 = makeHash!SHA1(randomPassword());
975 
976 	assert(resultRand1.salt != result1.salt);
977 
978 	assert(resultRand1.salt != resultRand2.salt);
979 	assert(resultRand1.hash != resultRand2.hash);
980 
981 	unitlog("Testing parseHash()");
982 	auto result2Parsed = parseDAuthHash( result2_512.toString() );
983 	assert(result2_512.salt       == result2Parsed.salt);
984 	assert(result2_512.hash       == result2Parsed.hash);
985 	assert(result2_512.toString() == result2Parsed.toString());
986 
987 	assert(makeHash(result2Parsed.digest, plainText1, result2Parsed.salt) == result2Parsed);
988 	assertThrown!ConvException(parseDAuthHash( result2_512.toCryptString() ));
989 	assert(parseHash( result2_512.toString() ).salt            == parseDAuthHash( result2_512.toString() ).salt);
990 	assert(parseHash( result2_512.toString() ).hash            == parseDAuthHash( result2_512.toString() ).hash);
991 	assert(parseHash( result2_512.toString() ).toString()      == parseDAuthHash( result2_512.toString() ).toString());
992 	assert(parseHash( result2_512.toString() ).toCryptString() == parseDAuthHash( result2_512.toString() ).toCryptString());
993 	
994 	unitlog("Testing parseHash() - crypt(3)");
995 	auto result2ParsedCrypt = parseCryptHash( result2_512.toCryptString() );
996 	assert(result2_512.salt       == result2ParsedCrypt.salt);
997 	assert(result2_512.hash       == result2ParsedCrypt.hash);
998 	assert(result2_512.toString() == result2ParsedCrypt.toString());
999 
1000 	assert(makeHash(result2ParsedCrypt.digest, plainText1, result2ParsedCrypt.salt) == result2ParsedCrypt);
1001 	assertThrown!ConvException(parseCryptHash( result2_512.toString() ));
1002 	assert(parseHash( result2_512.toCryptString() ).salt            == parseCryptHash( result2_512.toCryptString() ).salt);
1003 	assert(parseHash( result2_512.toCryptString() ).hash            == parseCryptHash( result2_512.toCryptString() ).hash);
1004 	assert(parseHash( result2_512.toCryptString() ).toString()      == parseCryptHash( result2_512.toCryptString() ).toString());
1005 	assert(parseHash( result2_512.toCryptString() ).toCryptString() == parseCryptHash( result2_512.toCryptString() ).toCryptString());
1006 	
1007 	auto desCryptHash = "sa5JEXtYx/rm6";
1008 	assertThrown!UnknownDigestException( parseHash(desCryptHash) );
1009 	assert(collectExceptionMsg( parseHash(desCryptHash) ).canFind("DES"));
1010 
1011 	unitlog("Testing isSameHash");
1012 	assert(isSameHash     (plainText1, result2));
1013 	assert(isSameHash!SHA1(plainText1, result2.hash, result2.salt));
1014 	assert(isSameHash     (plainText1, result2.hash, result2.salt, new SHA1Digest()));
1015 
1016 	assert(isSameHash!SHA1(plainText1, result2AltSalter, &altSalter!SHA1));
1017 	assert(isSameHash!SHA1(plainText1, result2AltSalter.hash, result2AltSalter.salt, &altSalter!SHA1));
1018 	assert(isSameHash     (plainText1, result2AltSalter.hash, result2AltSalter.salt, new SHA1Digest(), &altSalter!Digest));
1019 
1020 	assert(!isSameHash     (dupPassword("bad pass"), result2));
1021 	assert(!isSameHash!SHA1(dupPassword("bad pass"), result2.hash, result2.salt));
1022 	assert(!isSameHash     (dupPassword("bad pass"), result2.hash, result2.salt, new SHA1Digest()));
1023 
1024 	assert(!isSameHash!SHA1(dupPassword("bad pass"), result2AltSalter, &altSalter!SHA1));
1025 	assert(!isSameHash!SHA1(dupPassword("bad pass"), result2AltSalter.hash, result2AltSalter.salt, &altSalter!SHA1));
1026 	assert(!isSameHash     (dupPassword("bad pass"), result2AltSalter.hash, result2AltSalter.salt, new SHA1Digest(), &altSalter!Digest));
1027 	
1028 	Hash!SHA1Digest ooHashSHA1Digest;
1029 	ooHashSHA1Digest.salt = result2.salt;
1030 	ooHashSHA1Digest.hash = result2.hash;
1031 	ooHashSHA1Digest.digest = new SHA1Digest();
1032 	assert( isSameHash(plainText1, ooHashSHA1Digest) );
1033 	ooHashSHA1Digest.digest = null;
1034 	assert( isSameHash(plainText1, ooHashSHA1Digest) );
1035 	
1036 	Hash!Digest ooHashDigest;
1037 	ooHashDigest.salt = result2.salt;
1038 	ooHashDigest.hash = result2.hash;
1039 	ooHashDigest.digest = new SHA1Digest();
1040 	assert( isSameHash(plainText1, ooHashDigest) );
1041 	ooHashDigest.digest = null;
1042 	assertThrown!UnknownDigestException( isSameHash(plainText1, ooHashDigest) );
1043 	
1044 	assert( isSameHash(plainText1, parseHash(result2.toString())) );
1045 
1046 	auto wrongSalt = result2;
1047 	wrongSalt.salt = wrongSalt.salt[4..$-1];
1048 	
1049 	assert(!isSameHash     (plainText1, wrongSalt));
1050 	assert(!isSameHash!SHA1(plainText1, wrongSalt.hash, wrongSalt.salt));
1051 	assert(!isSameHash     (plainText1, wrongSalt.hash, wrongSalt.salt, new SHA1Digest()));
1052 
1053 	Hash!MD5 wrongDigest;
1054 	wrongDigest.salt = result2.salt;
1055 	wrongDigest.hash = cast(ubyte[16])result2.hash[0..16];
1056 	
1057 	assert(!isSameHash    (plainText1, wrongDigest));
1058 	assert(!isSameHash!MD5(plainText1, wrongDigest.hash, wrongDigest.salt));
1059 	assert(!isSameHash    (plainText1, wrongDigest.hash, wrongDigest.salt, new MD5Digest()));
1060 }
1061 
1062 /++
1063 Compare two arrays in "length-constant" time. This thwarts timing-based
1064 attacks by guaranteeing all comparisons (of a given length) take the same
1065 amount of time.
1066 
1067 See the section "Why does the hashing code on this page compare the hashes in
1068 "length-constant" time?" at:
1069     $(LINK https://crackstation.net/hashing-security.htm)
1070 +/
1071 bool lengthConstantEquals(ubyte[] a, ubyte[] b)
1072 {
1073 	auto diff = a.length ^ b.length;
1074 	for(int i = 0; i < a.length && i < b.length; i++)
1075 		diff |= a[i] ^ b[i];
1076 
1077 	return diff == 0;
1078 }
1079 
1080 // Borrowed from Phobos (TemplateArgsOf only exists in DMD 2.066 and up).
1081 package template DAuth_TemplateArgsOf(alias T : Base!Args, alias Base, Args...)
1082 {
1083 	alias DAuth_TemplateArgsOf = Args;
1084 }
1085 package template DAuth_TemplateArgsOf(T : Base!Args, alias Base, Args...)
1086 {
1087 	alias DAuth_TemplateArgsOf = Args;
1088 }
1089 static assert(is( DAuth_TemplateArgsOf!( Hash!SHA1   )[0] == SHA1   ));
1090 static assert(is( DAuth_TemplateArgsOf!( Hash!Digest )[0] == Digest ));
1091 
1092 private struct dummy(T) {}
1093 static if(!is(std.traits.TemplateArgsOf!(dummy!int)))
1094 	private alias TemplateArgsOf = DAuth_TemplateArgsOf;