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