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 }