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 }