1 /** 2 SASL authentication functions 3 4 Copyright: © 2012-2016 Nicolas Gurrola 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Nicolas Gurrola 7 */ 8 module vibe.db.mongo.sasl; 9 10 import std.algorithm; 11 import std.base64; 12 import std.conv; 13 import std.digest.hmac; 14 import std.digest.sha; 15 import std.exception; 16 import std.format; 17 import std.string; 18 import std.utf; 19 import vibe.crypto.cryptorand; 20 21 @safe: 22 23 private SHA1HashMixerRNG g_rng() 24 { 25 static SHA1HashMixerRNG m_rng; 26 if (!m_rng) m_rng = new SHA1HashMixerRNG; 27 return m_rng; 28 } 29 30 package struct ScramState 31 { 32 @safe: 33 34 private string m_firstMessageBare; 35 private string m_nonce; 36 private DigestType!SHA1 m_saltedPassword; 37 private string m_authMessage; 38 39 string createInitialRequest(string user) 40 { 41 ubyte[18] randomBytes; 42 g_rng.read(randomBytes[]); 43 m_nonce = Base64.encode(randomBytes); 44 45 m_firstMessageBare = format("n=%s,r=%s", escapeUsername(user), m_nonce); 46 return format("n,,%s", m_firstMessageBare); 47 } 48 49 string update(string password, string challenge) 50 { 51 string serverFirstMessage = challenge; 52 53 string next = challenge.find(','); 54 if (challenge.length < 2 || challenge[0 .. 2] != "r=" || next.length < 3 || next[1 .. 3] != "s=") 55 throw new Exception("Invalid server challenge format"); 56 string serverNonce = challenge[2 .. $ - next.length]; 57 challenge = next[3 .. $]; 58 next = challenge.find(','); 59 ubyte[] salt = Base64.decode(challenge[0 .. $ - next.length]); 60 61 if (next.length < 3 || next[1 .. 3] != "i=") 62 throw new Exception("Invalid server challenge format"); 63 int iterations = next[3 .. $].to!int(); 64 65 if (serverNonce[0 .. m_nonce.length] != m_nonce) 66 throw new Exception("Invalid server nonce received"); 67 string finalMessage = format("c=biws,r=%s", serverNonce); 68 69 m_saltedPassword = pbkdf2(password.representation, salt, iterations); 70 m_authMessage = format("%s,%s,%s", m_firstMessageBare, serverFirstMessage, finalMessage); 71 72 auto proof = getClientProof(m_saltedPassword, m_authMessage); 73 return format("%s,p=%s", finalMessage, Base64.encode(proof)); 74 } 75 76 string finalize(string challenge) 77 { 78 if (challenge.length < 2 || challenge[0 .. 2] != "v=") 79 { 80 throw new Exception("Invalid server signature format"); 81 } 82 if (!verifyServerSignature(Base64.decode(challenge[2 .. $]), m_saltedPassword, m_authMessage)) 83 { 84 throw new Exception("Invalid server signature"); 85 } 86 return null; 87 } 88 89 private static string escapeUsername(string user) 90 { 91 char[] buffer; 92 foreach (i, dchar ch; user) 93 { 94 if (ch == ',' || ch == '=') { 95 if (!buffer) { 96 buffer.reserve(user.length + 2); 97 buffer ~= user[0 .. i]; 98 } 99 if (ch == ',') 100 buffer ~= "=2C"; 101 else 102 buffer ~= "=3D"; 103 } else if (buffer) 104 encode(buffer, ch); 105 } 106 return buffer ? () @trusted { return assumeUnique(buffer); } () : user; 107 } 108 109 unittest 110 { 111 string user = "user"; 112 assert(escapeUsername(user) == user); 113 assert(escapeUsername(user) is user); 114 assert(escapeUsername("user,1") == "user=2C1"); 115 assert(escapeUsername("user=1") == "user=3D1"); 116 assert(escapeUsername("u,=ser1") == "u=2C=3Dser1"); 117 assert(escapeUsername("u=se=r1") == "u=3Dse=3Dr1"); 118 } 119 120 private static auto getClientProof(DigestType!SHA1 saltedPassword, string authMessage) 121 { 122 auto clientKey = () @trusted { return hmac!SHA1("Client Key".representation, saltedPassword); } (); 123 auto storedKey = sha1Of(clientKey); 124 auto clientSignature = () @trusted { return hmac!SHA1(authMessage.representation, storedKey); } (); 125 126 foreach (i; 0 .. clientKey.length) 127 { 128 clientKey[i] = clientKey[i] ^ clientSignature[i]; 129 } 130 return clientKey; 131 } 132 133 private static bool verifyServerSignature(ubyte[] signature, DigestType!SHA1 saltedPassword, string authMessage) 134 @trusted { 135 auto serverKey = hmac!SHA1("Server Key".representation, saltedPassword); 136 auto serverSignature = hmac!SHA1(authMessage.representation, serverKey); 137 return serverSignature == signature; 138 } 139 } 140 141 private DigestType!SHA1 pbkdf2(const ubyte[] password, const ubyte[] salt, int iterations) 142 { 143 import std.bitmanip; 144 145 ubyte[4] intBytes = [0, 0, 0, 1]; 146 auto last = () @trusted { return hmac!SHA1(salt, intBytes[], password); } (); 147 auto current = last; 148 foreach (i; 1 .. iterations) 149 { 150 last = () @trusted { return hmac!SHA1(last[], password); } (); 151 foreach (j; 0 .. current.length) 152 { 153 current[j] = current[j] ^ last[j]; 154 } 155 } 156 return current; 157 }