1 /**
2 	Poul-Henning Kamp's MD5 crypt(3) algorithm
3 
4 	This algorithm is from the 90s and there's no good reason to use it in new applications.  It's only here because it's a widely supported crypt(3) algorithm that's still not totally insecure.
5 */
6 module passwd.md5;
7 
8 /*
9 	This file references the original implementation at from https://svnweb.freebsd.org/base/head/lib/libcrypt/crypt.c?revision=4246&view=markup
10 
11 	That implementation contains the following notice:
12 
13 	 * ----------------------------------------------------------------------------
14 	 * "THE BEER-WARE LICENSE" (Revision 42):
15 	 * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
16 	 * can do whatever you want with this stuff. If we meet some day, and you think
17 	 * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
18 	 * ----------------------------------------------------------------------------
19 */
20 
21 @safe:
22 
23 import std.digest.md;
24 import std.range.primitives;
25 public import std.typecons : Flag, No, Yes;
26 
27 import passwd.util;
28 
29 /// Routines for MD5-based crypt(3)
30 struct MD5Crypt
31 {
32 	/// Maximum length needed for output of genSalt()
33 	enum kMaxSaltStrLength = "$1$".length + 8;
34 	/// Maximum length needed for output of crypt()
35 	enum kMaxCryptStrLength = kMaxSaltStrLength + 1 + kDigestLength * 8 / 6;
36 
37 	/// Generate a good salt for this algorithm
38 	static string genSalt()
39 	{
40 		import std.array : appender;
41 		auto ret_app = appender!string;
42 		MD5Crypt.genSalt(ret_app);
43 		return ret_app[];
44 	}
45 
46 	/// Generate a good salt for this algorithm and write to an output range
47 	static void genSalt(Out)(ref Out output) if (isOutputRange!(Out, char))
48 	{
49 		foreach (char c; "$1$") output.put(c);
50 		ubyte[6] random_buf;
51 		fillSecureRandom(random_buf[]);
52 		random_buf.cryptB64Encode(output);
53 	}
54 
55 	/// Hash password and write full crypt(3) string or just encoded digest to an output range
56 	static void crypt(Out)(const(char)[] password, ref Out output, ref const(CryptPieces) salt_data, Flag!"writeSalt" write_salt = Yes.writeSalt) if (isOutputRange!(Out, char))
57 	{
58 		if (write_salt)
59 		{
60 			foreach (char c; "$1$") output.put(c);
61 			foreach (char c; truncateSalt(salt_data.salt_txt)) output.put(c);
62 			output.put('$');
63 		}
64 		auto digest = digestOf(password, salt_data);
65 		digest.cryptB64Encode(output);
66 	}
67 
68 	private:
69 
70 	enum kDigestLength = 16;
71 	alias Digest = ubyte[kDigestLength];
72 
73 	/**
74 		Enforce maximum salt string length
75 
76 		Anything longer is silently ignored.
77 	*/
78 	static const(char)[] truncateSalt(const(char)[] salt_txt) pure
79 	{
80 		import std.algorithm.comparison : min;
81 		return salt_txt[0..min(8, $)];
82 	}
83 
84 	/// The heavy lifting of hashing the password to a binary digest
85 	static Digest digestOf(const(char)[] password, ref const(CryptPieces) salt_data)
86 	{
87 		import std..string : representation;
88 		auto salt_bytes = truncateSalt(salt_data.salt_txt).representation;
89 		auto password_bytes = password.representation;
90 
91 		// See also Ulrich Drepper's spec for the similar SHA-based crypt
92 
93 		// Ultimately, this algorithm is a grab bag of things done to mix up the input and not be fast about it.
94 		// Ours not to reason why. Just reimplement what PHK did.
95 
96 		MD5 ctx, ctx1;
97 		scope (exit) { secureWipe(ctx); secureWipe(ctx1); }
98 		Digest digest;  // "final" in PHK's code, but that's a reserved word in D
99 
100 		ctx.put(password_bytes);
101 		ctx.put("$1$".representation);
102 		ctx.put(salt_bytes);
103 
104 		// A second hash gets mixed into the first
105 		ctx1.put(password_bytes);
106 		ctx1.put(salt_bytes);
107 		ctx1.put(password_bytes);
108 		digest = ctx1.finish();
109 		ctx.stretchPut(digest[], password.length);
110 
111 		// "Then something really weird..."
112 		for (size_t i = password.length; i; i >>= 1)
113 		{
114 			// It looks like there are a couple of bugs in the original PHK code, but they became part of the standard MD5 crypt hash
115 			if (i & 1)
116 			{
117 				// In the PHK code, this was taken from "final" (digest), but that got zeroed out a few lines earlier
118 				ctx.put(cast(ubyte)0);
119 			}
120 			else
121 			{
122 				// In the PHK code, this was actually the jth element, but j was never updated from zero
123 				ctx.put(password_bytes[0]);
124 			}
125 		}
126 
127 		digest = ctx.finish();
128 
129 		// Apparently 1000 rounds took 34ms on PHK's 60Mhz Pentium back in 1994
130 		foreach (i; 0..1000)
131 		{
132 			ctx1.start();
133 			if (i & 1)
134 			{
135 				ctx1.put(password_bytes);
136 			}
137 			else
138 			{
139 				ctx1.put(digest[]);
140 			}
141 
142 			if (i % 3) ctx1.put(salt_bytes);
143 			if (i % 7) ctx1.put(password_bytes);
144 
145 			if (i & 1)
146 			{
147 				ctx1.put(digest[]);
148 			}
149 			else
150 			{
151 				ctx1.put(password_bytes);
152 			}
153 
154 			digest = ctx1.finish();
155 		}
156 
157 		// Finally the output gets permuted
158 		foreach (j; 0..output_swaps.length)
159 		{
160 			import std.algorithm.mutation : swap;
161 			swap(digest[j], digest[output_swaps[j]]);
162 		}
163 
164 		return digest;
165 	}
166 }
167 
168 private:
169 
170 immutable(size_t[]) output_swaps = permSwapDecomposition([
171 	12, 6, 0,
172 	13, 7, 1,
173 	14, 8, 2,
174 	15, 9, 3,
175 	5, 10, 4,
176 	11
177 ]);