1 /** 2 Library for UNIX-style password hashing. 3 4 Basic example of password hashing: 5 --- 6 const salt = SHA512Crypt.genSalt(); 7 // Result looks something like "$6$/CrouvED7qMJ/IbD" 8 auto crypted = "hunter2".crypt(salt); 9 // Result looks something like "$6$/CrouvED7qMJ/IbD$w2auDz2o61BBLowCbYbO.AIsM5XxSPME3PW2b7P.3qamDP5v4aSwyBPLDKolI/rBjTTGDIhUfUsszNv/DOy0B." 10 --- 11 12 The interface to each algorithm is a struct with static members, as below: 13 --- 14 struct CryptAlgo 15 { 16 /// Length in bytes of a binary digest 17 enum kDigestLength; 18 /// Maximum length needed for output of genSalt() 19 enum kMaxSaltStrLength; 20 /// Maximum length needed for output of crypt() 21 enum kMaxCryptStrLength; 22 23 /// Generate a good salt for this algorithm 24 static string genSalt(); 25 26 /// Generate a good salt for this algorithm and write to an output range 27 static void genSalt(Out)(ref Out output) if (isOutputRange!(Out, char)); 28 } 29 --- 30 */ 31 module passwd; 32 33 @safe: 34 35 import std.algorithm.comparison : max; 36 import std.algorithm.searching : findSplit; 37 import std.digest : secureEqual; 38 import std.meta : AliasSeq, staticMap; 39 import std.range; 40 public import std.typecons : Flag, No, Yes; 41 import std.utf : byCodeUnit; 42 43 import passwd.bcrypt; 44 import passwd.exception; 45 import passwd.md5; 46 import passwd.sha; 47 import passwd.util; 48 49 /** 50 Hash `password` according to `salt` 51 52 It's a D version of the standard crypt(3) function 53 https://www.freebsd.org/cgi/man.cgi?crypt%283%29 54 55 It's recommended that `salt` be generated using one of the provided `genSalt()` functions. (E.g., `SHA512Crypt.genSalt()`) 56 57 Overloads are provided that allow writing to a given output range, or using a pre-parsed salt string (see `passwd.util.cryptSplit()`), or optionally only writing the encoded digest (without the salt). Most users won't need them. 58 59 Note: crypt(3) allows algorithms to sanitise the salt string, so the output isn't guaranteed to be the input salt string concatenated with the encoded digest, unless the salt string was generated correctly by (for example) the provided `genSalt()` functions. 60 */ 61 char[] crypt(const(char)[] password, const(char)[] salt) 62 { 63 auto ret_app = appender!(char[]); 64 password.crypt(ret_app, salt); 65 return ret_app[]; 66 } 67 68 /// ditto 69 void crypt(Out)(const(char)[] password, ref Out output, const(char)[] salt, Flag!"writeSalt" write_salt = Yes.writeSalt) if (isOutputRange!(Out, char)) 70 { 71 auto salt_data = cryptSplit(salt); 72 password.crypt(output, salt_data, write_salt); 73 } 74 75 /// ditto 76 void crypt(Out)(const(char)[] password, ref Out output, ref const(CryptPieces) salt_data, Flag!"writeSalt" write_salt = Yes.writeSalt) if (isOutputRange!(Out, char)) 77 { 78 switch (salt_data.algo_id) 79 { 80 case "1": 81 MD5Crypt.crypt(password, output, salt_data, write_salt); 82 return; 83 case "2a": 84 case "2b": 85 Bcrypt.crypt(password, output, salt_data, write_salt); 86 return; 87 case "5": 88 SHA256Crypt.crypt(password, output, salt_data, write_salt); 89 return; 90 case "6": 91 SHA512Crypt.crypt(password, output, salt_data, write_salt); 92 return; 93 default: 94 throw new NotImplementedException("crypt() algorithm not implemented"); 95 } 96 } 97 98 /// 99 unittest 100 { 101 const salt = SHA512Crypt.genSalt(); 102 // Result looks something like "$6$/CrouvED7qMJ/IbD" 103 auto crypted = "hunter2".crypt(salt); 104 // Result looks something like "$6$/CrouvED7qMJ/IbD$w2auDz2o61BBLowCbYbO.AIsM5XxSPME3PW2b7P.3qamDP5v4aSwyBPLDKolI/rBjTTGDIhUfUsszNv/DOy0B." 105 106 // crypt(3)ed passwords can be erased from memory after use if you are paranoid 107 secureWipe(crypted); 108 import std.algorithm.searching : all; 109 assert (crypted.all!"a == 0"); 110 } 111 112 /** 113 Test a password against the given crypt(3) string 114 115 This is the recommended way to check an untrusted user's password guess against a password database. The naïve method of `crypt()`ing the password and comparing it using == is vulnerable to a timing attack that leaks the hashed password, allowing the attacker to run their own guesses offline. 116 117 An attacker timing the response of this function can guess the algorithm used for hashing, but not easily figure out the hash or right password. 118 */ 119 bool canCryptTo(const(char)[] password, const(char)[] crypted) 120 { 121 char[kMaxCryptStrLength] buffer; 122 auto buffer_p = buffer[].byCodeUnit; 123 auto crypted_pieces = cryptSplit(crypted); 124 password.crypt(buffer_p, crypted_pieces, No.writeSalt); 125 auto digest_txt = buffer[0..$-buffer_p.length]; 126 return secureEqual(digest_txt.byCodeUnit, crypted_pieces.digest_txt.byCodeUnit); 127 } 128 129 /// 130 unittest 131 { 132 import std.stdio; 133 const password_guess = "hunter2"; 134 const crypted = "$1$ZjzxeLeq$WXTi8xm9qRouh1zB8tyxX0"; 135 if (password_guess.canCryptTo(crypted)) 136 { 137 // welcomeUserIn(); 138 } 139 else 140 { 141 // kickUserOut(); 142 assert (false); 143 } 144 } 145 146 private template GetMember(string name) 147 { 148 template GetMember(S) 149 { 150 enum GetMember = __traits(getMember, S, name); 151 } 152 } 153 /// Maximum size needed for any genSalt() result 154 enum kMaxSaltStrLength = max(staticMap!(GetMember!"kMaxSaltStrLength", CryptAlgos)); 155 /// Maximum size needed for any crypt() result 156 enum kMaxCryptStrLength = max(staticMap!(GetMember!"kMaxCryptStrLength", CryptAlgos)); 157 158 /// All supported crypt(3) algorithms 159 alias CryptAlgos = AliasSeq!(MD5Crypt, Bcrypt, SHA256Crypt, SHA512Crypt);