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