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 }