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