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;