1 /++ 2 DAuth - Salted Hashed Password Library for D 3 Random generators 4 +/ 5 6 module dauth.random; 7 8 import std.algorithm; 9 import std.array; 10 import ascii = std.ascii; 11 import std.base64; 12 import std.conv; 13 import std.digest.crc; 14 import std.digest.md; 15 import std.digest.ripemd; 16 import std.digest.sha; 17 import std.exception; 18 import std.functional; 19 import std.random; 20 import std.range; 21 import std.typecons; 22 23 import dauth.core; 24 import dauth.hashdrbg; 25 26 /// In characters. Default length of randomly-generated passwords. 27 enum defaultPasswordLength = 20; 28 29 /++ 30 Punctuation is not included in generated passwords by default. Technically, 31 this is slightly less secure for a given password length, but it prevents 32 syntax-related bugs when a generated password is stored in a (properly-secured) 33 text-based configuration file. 34 35 If you know how the generated password will be used, you can add known-safe 36 punctuation to this when you call randomPassword. 37 +/ 38 enum defaultPasswordChars = cast(immutable(ubyte)[]) (ascii.letters ~ ascii.digits); 39 40 /// In bytes. Must be a multiple of 4. 41 enum defaultSaltLength = 32; 42 43 /++ 44 In bytes of randomness, not length of token. 45 Must be a multiple of 4. Although, due to usage of base64, using a multiple 46 of 12 prevents a padding tilde from existing at the end of every token. 47 +/ 48 enum defaultTokenStrength = 36; 49 50 /++ 51 RNGs used with DAuth must be either a isRandomStream, or 52 a isUniformRNG input range that emits uint values. 53 +/ 54 enum isDAuthRandom(T) = 55 isRandomStream!T || 56 (isUniformRNG!T && is(ElementType!T == uint)); 57 58 /++ 59 Generates a random password. 60 61 This is limited to generating ASCII passwords. This is because including 62 non-ASCII characters in generated passwords is more complex, more error-prone, 63 more likely to trigger latent unicode bugs in other systems, and not 64 particularly useful anyway. 65 66 Throws an Exception if passwordChars.length isn't at least 2. 67 68 USE THIS RESPONSIBLY! NEVER EMAIL THE PASSWORD! Emailing a generated password 69 to a user is a highly insecure practice and should NEVER be done. However, 70 there are other times when generating a password may be reasonable, so this is 71 provided as a convenience. 72 73 Optional_Params: 74 Rand - Default value is 'DefaultCryptoRand' 75 76 length - Default value is 'defaultPasswordLength' 77 78 passwordChars - Default value is 'defaultPasswordChars' 79 +/ 80 Password randomPassword(Rand = DefaultCryptoRand) ( 81 size_t length = defaultPasswordLength, 82 const(ubyte)[] passwordChars = defaultPasswordChars 83 ) 84 if(isDAuthRandom!Rand) 85 out(result) 86 { 87 assert(result.length == length); 88 } 89 body 90 { 91 Rand rand; 92 rand.initRand(); 93 return randomPassword(rand, length, passwordChars); 94 } 95 96 ///ditto 97 Password randomPassword(Rand = DefaultCryptoRand) ( 98 ref Rand rand, 99 size_t length = defaultPasswordLength, 100 const(ubyte)[] passwordChars = defaultPasswordChars 101 ) 102 if(isDAuthRandom!Rand) 103 out(result) 104 { 105 assert(result.length == length); 106 } 107 body 108 { 109 Appender!(ubyte[]) sink; 110 randomPassword(rand, sink, length, passwordChars); 111 return toPassword(sink.data); 112 } 113 114 ///ditto 115 void randomPassword(Rand = DefaultCryptoRand, Sink)( 116 ref Sink sink, 117 size_t length = defaultPasswordLength, 118 const(ubyte)[] passwordChars = defaultPasswordChars 119 ) 120 if( isDAuthRandom!Rand && isOutputRange!(Sink, ubyte) ) 121 { 122 Rand rand; 123 rand.initRand(); 124 randomPassword(rand, sink, length, passwordChars); 125 } 126 127 ///ditto 128 void randomPassword(Rand = DefaultCryptoRand, Sink) ( 129 ref Rand rand, ref Sink sink, 130 size_t length = defaultPasswordLength, 131 const(ubyte)[] passwordChars = defaultPasswordChars 132 ) 133 if( isDAuthRandom!Rand && isOutputRange!(Sink, ubyte) ) 134 { 135 enforce(passwordChars.length >= 2); 136 137 static if(isUniformRNG!Rand) 138 alias randRange = rand; 139 else 140 WrappedStreamRNG!(Rand, uint) randRange; 141 142 randRange.popFront(); // Ensure fresh data 143 foreach(i; 0..length) 144 { 145 auto charIndex = randRange.front % passwordChars.length; 146 sink.put(passwordChars[charIndex]); 147 randRange.popFront(); 148 } 149 } 150 151 version(DAuth_Unittest) 152 unittest 153 { 154 unitlog("Testing randomPassword"); 155 156 void validateChars(Password pass, immutable(ubyte)[] validChars, size_t length) 157 { 158 foreach(i; 0..pass.data.length) 159 { 160 assert( 161 validChars.canFind( cast(ubyte)pass.data[i] ), 162 text( 163 "Invalid char `", pass.data[i], 164 "` (ascii ", cast(ubyte)pass.data[i], ") at index ", i, 165 " in password length ", length, 166 ". Valid char set: ", validChars 167 ) 168 ); 169 } 170 } 171 172 // Ensure non-purity 173 assert(randomPassword() != randomPassword()); 174 assert(randomPassword!MinstdRand() != randomPassword!MinstdRand()); 175 auto randA = MinstdRand(unpredictableSeed); 176 auto randB = MinstdRand(unpredictableSeed); 177 assert(randomPassword(randA) != randomPassword(randB)); 178 179 // Ensure length, valid chars and non-purity: 180 // Default RNG, length and charset. Non-sink. 181 Password prevPass; 182 foreach(i; 0..10) 183 { 184 auto pass = randomPassword(); 185 assert(pass.length == defaultPasswordLength); 186 validateChars(pass, defaultPasswordChars, defaultPasswordLength); 187 188 assert(pass != prevPass); 189 prevPass = pass; 190 } 191 192 // Test argument-checking 193 assertThrown(randomPassword(5, [])); 194 assertThrown(randomPassword(5, ['X'])); 195 196 // Ensure length, valid chars and non-purity: 197 // Default and provided RNGs. With/without sink. Various lengths and charsets. 198 auto charsets = [ 199 defaultPasswordChars, 200 defaultPasswordChars ~ cast(immutable(ubyte)[])".,<>", 201 cast(immutable(ubyte)[]) "abc123", 202 cast(immutable(ubyte)[]) "XY" 203 ]; 204 foreach(validChars; charsets) 205 foreach(length; [defaultPasswordLength, 5, 2]) 206 foreach(i; 0..2) 207 { 208 Password pass; 209 MinstdRand rand; 210 Appender!(ubyte[]) sink; 211 212 // -- Non-sink ------------- 213 214 // Default RNG 215 pass = randomPassword(length, validChars); 216 assert(pass.length == length); 217 validateChars(pass, validChars, length); 218 if(validChars.length > 25) 219 assert(pass != randomPassword(length, validChars)); 220 221 // Provided RNG type 222 pass = randomPassword!MinstdRand(length, validChars); 223 assert(pass.length == length); 224 validateChars(pass, validChars, length); 225 if(validChars.length > 25) 226 assert(pass != randomPassword!MinstdRand(length, validChars)); 227 228 // Provided RNG object 229 rand = MinstdRand(unpredictableSeed); 230 pass = randomPassword(rand, length, validChars); 231 assert(pass.length == length); 232 validateChars(pass, validChars, length); 233 if(validChars.length > 25) 234 assert(pass != randomPassword(rand, length, validChars)); 235 236 // -- With sink ------------- 237 238 // Default RNG 239 sink = appender!(ubyte[])(); 240 randomPassword(sink, length, validChars); 241 pass = toPassword(sink.data); 242 assert(pass.length == length); 243 validateChars(pass, validChars, length); 244 if(validChars.length > 25) 245 { 246 sink = appender!(ubyte[])(); 247 randomPassword(sink, length, validChars); 248 assert(pass.data != sink.data); 249 } 250 251 // Provided RNG type 252 sink = appender!(ubyte[])(); 253 randomPassword!MinstdRand(sink, length, validChars); 254 pass = toPassword(sink.data); 255 assert(pass.length == length); 256 validateChars(pass, validChars, length); 257 if(validChars.length > 25) 258 { 259 sink = appender!(ubyte[])(); 260 randomPassword!MinstdRand(sink, length, validChars); 261 assert(pass.data != sink.data); 262 } 263 264 // Provided RNG object 265 sink = appender!(ubyte[])(); 266 rand = MinstdRand(unpredictableSeed); 267 randomPassword(rand, sink, length, validChars); 268 pass = toPassword(sink.data); 269 assert(pass.length == length); 270 validateChars(pass, validChars, length); 271 if(validChars.length > 25) 272 { 273 sink = appender!(ubyte[])(); 274 randomPassword(rand, sink, length, validChars); 275 assert(pass.data != sink.data); 276 } 277 } 278 } 279 280 /++ 281 Generates a random salt. Necessary for salting passwords. 282 283 NEVER REUSE A SALT! This must be called separately EVERY time any user sets 284 or resets a password. Reusing salts defeats the security of salting passwords. 285 286 The length must be a multiple of 4, or this will throw an Exception 287 288 Optional_Params: 289 Rand - Default value is 'DefaultCryptoRand' 290 291 length - Default value is 'defaultSaltLength' 292 +/ 293 Salt randomSalt(Rand = DefaultCryptoRand)(size_t length = defaultSaltLength) 294 if(isDAuthRandom!Rand) 295 { 296 return randomBytes!Rand(length); 297 } 298 299 ///ditto 300 Salt randomSalt(Rand = DefaultCryptoRand)(ref Rand rand, size_t length = defaultSaltLength) 301 if(isDAuthRandom!Rand) 302 { 303 return randomBytes(length, rand); 304 } 305 306 version(DAuth_Unittest) 307 unittest 308 { 309 unitlog("Testing randomSalt"); 310 311 // Ensure non-purity 312 assert(randomSalt() != randomSalt()); 313 assert(randomSalt!MinstdRand() != randomSalt!MinstdRand()); 314 auto randA = MinstdRand(unpredictableSeed); 315 auto randB = MinstdRand(unpredictableSeed); 316 assert(randomSalt(randA) != randomSalt(randB)); 317 318 // Ensure zero-length case doesn't blow up 319 assert(randomSalt(0).empty); 320 assert(randomSalt(randA, 0).empty); 321 322 // Test argument-checking (length not multiple of 4) 323 assertThrown(randomSalt(5)); 324 assertThrown(randomSalt(6)); 325 assertThrown(randomSalt(7)); 326 327 // Ensure length and non-purity: 328 // Default and provided RNGs. Various lengths. 329 foreach(length; [defaultSaltLength, 20, 8]) 330 foreach(i; 0..2) 331 { 332 Salt salt; 333 334 // Default RNG 335 salt = randomSalt(length); 336 assert(salt.length == length); 337 assert(salt != randomSalt(length)); 338 339 // Provided RNG type 340 salt = randomSalt!MinstdRand(length); 341 assert(salt.length == length); 342 assert(salt != randomSalt!MinstdRand(length)); 343 344 // Provided RNG object 345 auto rand = MinstdRand(unpredictableSeed); 346 salt = randomSalt(rand, length); 347 assert(salt.length == length); 348 assert(salt != randomSalt(rand, length)); 349 } 350 } 351 352 /++ 353 Generates a random token. Useful for temporary one-use URLs, such as in 354 email confirmations. 355 356 The strength is the number of bytes of randomness in the token. 357 Note this is NOT the length of the token string returned, since this token is 358 base64-encoded (using an entirely URI-safe version that doesn't need escaping) 359 from the raw random bytes. 360 361 The strength must be a multiple of 4, or this will throw an Exception 362 363 Optional_Params: 364 Rand - Default value is 'DefaultCryptoRand' 365 366 strength - Default value is 'defaultTokenStrength' 367 +/ 368 string randomToken(Rand = DefaultCryptoRand)(size_t strength = defaultTokenStrength) 369 if(isDAuthRandom!Rand) 370 { 371 return TokenBase64.encode( randomBytes!Rand(strength) ); 372 } 373 374 ///ditto 375 string randomToken(Rand = DefaultCryptoRand)(ref Rand rand, size_t strength = defaultTokenStrength) 376 if(isDAuthRandom!Rand) 377 { 378 return TokenBase64.encode( randomBytes(strength, rand) ); 379 } 380 381 version(DAuth_Unittest) 382 unittest 383 { 384 unitlog("Testing randomToken"); 385 386 // Ensure non-purity 387 assert(randomToken() != randomToken()); 388 assert(randomToken!MinstdRand() != randomToken!MinstdRand()); 389 auto randA = MinstdRand(unpredictableSeed); 390 auto randB = MinstdRand(unpredictableSeed); 391 assert(randomToken(randA) != randomToken(randB)); 392 393 // Ensure zero-strength case doesn't blow up 394 assert(randomToken(0).empty); 395 assert(randomToken(randA, 0).empty); 396 397 // Test argument-checking (strength not multiple of 4) 398 assertThrown(randomToken(5)); 399 assertThrown(randomToken(6)); 400 assertThrown(randomToken(7)); 401 402 // Ensure length, valid chars and non-purity: 403 // Default and provided RNGs. Various lengths. 404 foreach(strength; [defaultTokenStrength, 20, 8]) 405 foreach(i; 0..2) 406 { 407 string token; 408 MinstdRand rand; 409 410 import std.math : ceil; 411 412 // Default RNG 413 token = randomToken(strength); 414 // 6 bits per Base64-encoded byte vs. 8 bits per input (strength) byte 415 // (with input length rounded up to the next multiple of 3) 416 assert(token.length * 6 == (ceil(strength/3.0L)*3) * 8); 417 assert(TokenBase64.decode(token)); 418 assert(token != randomToken(strength)); 419 420 // Provided RNG type 421 token = randomToken!MinstdRand(strength); 422 assert(token.length * 6 == (ceil(strength/3.0L)*3) * 8); 423 assert(TokenBase64.decode(token)); 424 assert(token != randomToken!MinstdRand(strength)); 425 426 // Provided RNG object 427 rand = MinstdRand(unpredictableSeed); 428 token = randomToken(rand, strength); 429 assert(token.length * 6 == (ceil(strength/3.0L)*3) * 8); 430 assert(TokenBase64.decode(token)); 431 assert(token != randomToken(rand, strength)); 432 } 433 } 434 435 /// numBytes must be a multiple of 4, or this will throw an Exception 436 ubyte[] randomBytes(Rand = DefaultCryptoRand)(size_t numBytes) 437 if(isDAuthRandom!Rand) 438 { 439 Rand rand; 440 rand.initRand(); 441 return randomBytes(numBytes, rand); 442 } 443 444 ///ditto 445 ubyte[] randomBytes(Rand = DefaultCryptoRand)(size_t numBytes, ref Rand rand) 446 if(isDAuthRandom!Rand) 447 out(result) 448 { 449 assert(result.length == numBytes); 450 } 451 body 452 { 453 enforce(numBytes % 4 == 0, "numBytes must be multiple of 4, not "~to!string(numBytes)); 454 455 static if(isRandomStream!Rand) 456 { 457 ubyte[] result; 458 result.length = numBytes; 459 rand.read(result); 460 return result; 461 } 462 else // Fallback to range version 463 { 464 rand.popFront(); // Ensure fresh data 465 return cast(ubyte[])( rand.take(numBytes/4).array() ); 466 } 467 } 468 469 private void initRand(Rand)(ref Rand rand) 470 if(isDAuthRandom!Rand) 471 { 472 static if(isSeedable!Rand) 473 rand.seed(unpredictableSeed); 474 }