1 /**
2 	Library for UNIX-style password hashing.
3 
4 	Basic example of password hashing:
5 	---
6 	const salt = SHA512Crypt.genSalt();
7 	// Result looks something like "$6$/CrouvED7qMJ/IbD"
8 	auto crypted = "hunter2".crypt(salt);
9 	// Result looks something like "$6$/CrouvED7qMJ/IbD$w2auDz2o61BBLowCbYbO.AIsM5XxSPME3PW2b7P.3qamDP5v4aSwyBPLDKolI/rBjTTGDIhUfUsszNv/DOy0B."
10 	---
11 	
12 	The interface to each algorithm is a struct with static members, as below:
13 	---
14 	struct CryptAlgo
15 	{
16 		/// Length in bytes of a binary digest
17 		enum kDigestLength;
18 		/// Maximum length needed for output of genSalt()
19 		enum kMaxSaltStrLength;
20 		/// Maximum length needed for output of crypt()
21 		enum kMaxCryptStrLength;
22 
23 		/// Generate a good salt for this algorithm
24 		static string genSalt();
25 
26 		/// Generate a good salt for this algorithm and write to an output range
27 		static void genSalt(Out)(ref Out output) if (isOutputRange!(Out, char));
28 	}
29 	---
30 */
31 module passwd;
32 
33 @safe:
34 
35 import std.algorithm.comparison : max;
36 import std.algorithm.searching : findSplit;
37 import std.digest : secureEqual;
38 import std.meta : AliasSeq, staticMap;
39 import std.range;
40 public import std.typecons : Flag, No, Yes;
41 import std.utf : byCodeUnit;
42 
43 import passwd.bcrypt;
44 import passwd.exception;
45 import passwd.md5;
46 import passwd.sha;
47 import passwd.util;
48 
49 /**
50 	Hash `password` according to `salt`
51 
52 	It's a D version of the standard crypt(3) function
53 	https://www.freebsd.org/cgi/man.cgi?crypt%283%29
54 
55 	It's recommended that `salt` be generated using one of the provided `genSalt()` functions.  (E.g., `SHA512Crypt.genSalt()`)
56 
57 	Overloads are provided that allow writing to a given output range, or using a pre-parsed salt string (see `passwd.util.cryptSplit()`), or optionally only writing the encoded digest (without the salt).  Most users won't need them.
58 
59 	Note: crypt(3) allows algorithms to sanitise the salt string, so the output isn't guaranteed to be the input salt string concatenated with the encoded digest, unless the salt string was generated correctly by (for example) the provided `genSalt()` functions.
60 */
61 char[] crypt(const(char)[] password, const(char)[] salt)
62 {
63 	auto ret_app = appender!(char[]);
64 	password.crypt(ret_app, salt);
65 	return ret_app[];
66 }
67 
68 /// ditto
69 void crypt(Out)(const(char)[] password, ref Out output, const(char)[] salt, Flag!"writeSalt" write_salt = Yes.writeSalt) if (isOutputRange!(Out, char))
70 {
71 	auto salt_data = cryptSplit(salt);
72 	password.crypt(output, salt_data, write_salt);
73 }
74 
75 ///	ditto
76 void crypt(Out)(const(char)[] password, ref Out output, ref const(CryptPieces) salt_data, Flag!"writeSalt" write_salt = Yes.writeSalt) if (isOutputRange!(Out, char))
77 {
78 	switch (salt_data.algo_id)
79 	{
80 		case "1":
81 			MD5Crypt.crypt(password, output, salt_data, write_salt);
82 			return;
83 		case "2a":
84 		case "2b":
85 			Bcrypt.crypt(password, output, salt_data, write_salt);
86 			return;
87 		case "5":
88 			SHA256Crypt.crypt(password, output, salt_data, write_salt);
89 			return;
90 		case "6":
91 			SHA512Crypt.crypt(password, output, salt_data, write_salt);
92 			return;
93 		default:
94 			throw new NotImplementedException("crypt() algorithm not implemented");
95 	}
96 }
97 
98 ///
99 unittest
100 {
101 	const salt = SHA512Crypt.genSalt();
102 	// Result looks something like "$6$/CrouvED7qMJ/IbD"
103 	auto crypted = "hunter2".crypt(salt);
104 	// Result looks something like "$6$/CrouvED7qMJ/IbD$w2auDz2o61BBLowCbYbO.AIsM5XxSPME3PW2b7P.3qamDP5v4aSwyBPLDKolI/rBjTTGDIhUfUsszNv/DOy0B."
105 
106 	// crypt(3)ed passwords can be erased from memory after use if you are paranoid
107 	secureWipe(crypted);
108 	import std.algorithm.searching : all;
109 	assert (crypted.all!"a == 0");
110 }
111 
112 /**
113 	Test a password against the given crypt(3) string
114 
115 	This is the recommended way to check an untrusted user's password guess against a password database.  The naïve method of `crypt()`ing the password and comparing it using == is vulnerable to a timing attack that leaks the hashed password, allowing the attacker to run their own guesses offline.
116 
117 	An attacker timing the response of this function can guess the algorithm used for hashing, but not easily figure out the hash or right password.
118 */
119 bool canCryptTo(const(char)[] password, const(char)[] crypted)
120 {
121 	char[kMaxCryptStrLength] buffer;
122 	auto buffer_p = buffer[].byCodeUnit;
123 	auto crypted_pieces = cryptSplit(crypted);
124 	password.crypt(buffer_p, crypted_pieces, No.writeSalt);
125 	auto digest_txt = buffer[0..$-buffer_p.length];
126 	return secureEqual(digest_txt.byCodeUnit, crypted_pieces.digest_txt.byCodeUnit);
127 }
128 
129 ///
130 unittest
131 {
132 	import std.stdio;
133 	const password_guess = "hunter2";
134 	const crypted = "$1$ZjzxeLeq$WXTi8xm9qRouh1zB8tyxX0";
135 	if (password_guess.canCryptTo(crypted))
136 	{
137 		// welcomeUserIn();
138 	}
139 	else
140 	{
141 		// kickUserOut();
142 		assert (false);
143 	}
144 }
145 
146 private template GetMember(string name)
147 {
148 	template GetMember(S)
149 	{
150 		enum GetMember = __traits(getMember, S, name);
151 	}
152 }
153 /// Maximum size needed for any genSalt() result
154 enum kMaxSaltStrLength = max(staticMap!(GetMember!"kMaxSaltStrLength", CryptAlgos));
155 /// Maximum size needed for any crypt() result
156 enum kMaxCryptStrLength = max(staticMap!(GetMember!"kMaxCryptStrLength", CryptAlgos));
157 
158 /// All supported crypt(3) algorithms
159 alias CryptAlgos = AliasSeq!(MD5Crypt, Bcrypt, SHA256Crypt, SHA512Crypt);