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 ]);