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 }