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.traits; 19 import std.utf; 20 import vibe.crypto.cryptorand; 21 22 @safe: 23 24 private SHA1HashMixerRNG g_rng() 25 { 26 static SHA1HashMixerRNG m_rng; 27 if (!m_rng) m_rng = new SHA1HashMixerRNG; 28 return m_rng; 29 } 30 31 package struct ScramState 32 { 33 @safe: 34 35 private string m_firstMessageBare; 36 private string m_nonce; 37 private DigestType!SHA1 m_saltedPassword; 38 private string m_authMessage; 39 40 string createInitialRequest(string user) 41 { 42 ubyte[18] randomBytes; 43 g_rng.read(randomBytes[]); 44 m_nonce = Base64.encode(randomBytes); 45 46 m_firstMessageBare = format("n=%s,r=%s", escapeUsername(user), m_nonce); 47 return format("n,,%s", m_firstMessageBare); 48 } 49 50 version (unittest) private string createInitialRequestWithFixedNonce(string user, string nonce) 51 { 52 m_nonce = nonce; 53 54 m_firstMessageBare = format("n=%s,r=%s", escapeUsername(user), m_nonce); 55 return format("n,,%s", m_firstMessageBare); 56 } 57 58 // MongoDB drivers require 4096 min iterations https://github.com/mongodb/specifications/blob/59390a7ab2d5c8f9c29b8af1775ff25915c44036/source/auth/auth.rst#scram-sha-1 59 string update(string password, string challenge, int minIterations = 4096) 60 { 61 string serverFirstMessage = challenge; 62 63 string next = challenge.find(','); 64 if (challenge.length < 2 || challenge[0 .. 2] != "r=" || next.length < 3 || next[1 .. 3] != "s=") 65 throw new Exception("Invalid server challenge format"); 66 string serverNonce = challenge[2 .. $ - next.length]; 67 challenge = next[3 .. $]; 68 next = challenge.find(','); 69 ubyte[] salt = Base64.decode(challenge[0 .. $ - next.length]); 70 71 if (next.length < 3 || next[1 .. 3] != "i=") 72 throw new Exception("Invalid server challenge format"); 73 int iterations = next[3 .. $].to!int(); 74 75 if (iterations < minIterations) 76 throw new Exception("Server must request at least " ~ minIterations.to!string ~ " iterations"); 77 78 if (serverNonce[0 .. m_nonce.length] != m_nonce) 79 throw new Exception("Invalid server nonce received"); 80 string finalMessage = format("c=biws,r=%s", serverNonce); 81 82 m_saltedPassword = pbkdf2(password.representation, salt, iterations); 83 m_authMessage = format("%s,%s,%s", m_firstMessageBare, serverFirstMessage, finalMessage); 84 85 auto proof = getClientProof(m_saltedPassword, m_authMessage); 86 return format("%s,p=%s", finalMessage, Base64.encode(proof)); 87 } 88 89 string finalize(string challenge) 90 { 91 if (challenge.length < 2 || challenge[0 .. 2] != "v=") 92 { 93 throw new Exception("Invalid server signature format"); 94 } 95 if (!verifyServerSignature(Base64.decode(challenge[2 .. $]), m_saltedPassword, m_authMessage)) 96 { 97 throw new Exception("Invalid server signature"); 98 } 99 return null; 100 } 101 102 private static string escapeUsername(string user) 103 { 104 char[] buffer; 105 foreach (i, dchar ch; user) 106 { 107 if (ch == ',' || ch == '=') { 108 if (!buffer) { 109 buffer.reserve(user.length + 2); 110 buffer ~= user[0 .. i]; 111 } 112 if (ch == ',') 113 buffer ~= "=2C"; 114 else 115 buffer ~= "=3D"; 116 } else if (buffer) 117 encode(buffer, ch); 118 } 119 return buffer ? () @trusted { return assumeUnique(buffer); } () : user; 120 } 121 122 unittest 123 { 124 string user = "user"; 125 assert(escapeUsername(user) == user); 126 assert(escapeUsername(user) is user); 127 assert(escapeUsername("user,1") == "user=2C1"); 128 assert(escapeUsername("user=1") == "user=3D1"); 129 assert(escapeUsername("u,=ser1") == "u=2C=3Dser1"); 130 assert(escapeUsername("u=se=r1") == "u=3Dse=3Dr1"); 131 } 132 133 private static auto getClientProof(DigestType!SHA1 saltedPassword, string authMessage) 134 { 135 auto clientKey = () @trusted { return hmac!SHA1("Client Key".representation, saltedPassword); } (); 136 auto storedKey = sha1Of(clientKey); 137 auto clientSignature = () @trusted { return hmac!SHA1(authMessage.representation, storedKey); } (); 138 139 foreach (i; 0 .. clientKey.length) 140 { 141 clientKey[i] = clientKey[i] ^ clientSignature[i]; 142 } 143 return clientKey; 144 } 145 146 private static bool verifyServerSignature(ubyte[] signature, DigestType!SHA1 saltedPassword, string authMessage) 147 @trusted { 148 auto serverKey = hmac!SHA1("Server Key".representation, saltedPassword); 149 auto serverSignature = hmac!SHA1(authMessage.representation, serverKey); 150 return serverSignature == signature; 151 } 152 } 153 154 private DigestType!SHA1 pbkdf2(const ubyte[] password, const ubyte[] salt, int iterations) 155 { 156 import std.bitmanip; 157 158 ubyte[4] intBytes = [0, 0, 0, 1]; 159 auto last = () @trusted { return hmac!SHA1(salt, intBytes[], password); } (); 160 static assert(isStaticArray!(typeof(last)), 161 "Code is written so that the hash array is expected to be placed on the stack"); 162 auto current = last; 163 foreach (i; 1 .. iterations) 164 { 165 last = () @trusted { return hmac!SHA1(last[], password); } (); 166 foreach (j; 0 .. current.length) 167 { 168 current[j] = current[j] ^ last[j]; 169 } 170 } 171 return current; 172 } 173 174 unittest 175 { 176 // https://github.com/mongodb/specifications/blob/59390a7ab2d5c8f9c29b8af1775ff25915c44036/source/auth/auth.rst#id5 177 178 import vibe.db.mongo.settings : MongoClientSettings; 179 180 ScramState state; 181 assert(state.createInitialRequestWithFixedNonce("user", "fyko+d2lbbFgONRv9qkxdawL") 182 == "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"); 183 auto last = state.update(MongoClientSettings.makeDigest("user", "pencil"), 184 "r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,s=rQ9ZY3MntBeuP3E1TDVC4w==,i=10000"); 185 assert(last == "c=biws,r=fyko+d2lbbFgONRv9qkxdawLHo+Vgk7qvUOKUwuWLIWg4l/9SraGMHEE,p=MC2T8BvbmWRckDw8oWl5IVghwCY=", 186 last); 187 last = state.finalize("v=UMWeI25JD1yNYZRMpZ4VHvhZ9e0="); 188 assert(last == "", last); 189 }