Skip to content

Commit 83c2612

Browse files
committed
CSHARP-603: GSSAPI on linux using libgsasl.
1 parent 95b83dc commit 83c2612

14 files changed

+467
-577
lines changed

MongoDB.Driver/Communication/Security/Authenticator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private void Authenticate(MongoCredential credential, List<string> serverSupport
8282
}
8383
}
8484

85-
var message = string.Format("Unable to negotiate a protocol to authenticate. Credential for source {0}, username {1} over protocol {2} could not be authenticated", credential.Source, credential.Username, credential.AuthenticationProtocol);
85+
var message = string.Format("Unable to negotiate a protocol to authenticate. The credential for source {0}, username {1} over protocol {2} could not be authenticated.", credential.Source, credential.Username, credential.AuthenticationProtocol);
8686
throw new MongoSecurityException(message);
8787
}
8888

@@ -109,4 +109,4 @@ private List<string> GetServerSupportedMethods()
109109
}
110110
}
111111

112-
}
112+
}

MongoDB.Driver/Communication/Security/Mechanisms/CramMD5Mechanism.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
namespace MongoDB.Driver.Communication.Security.Mechanisms
2424
{
2525
/// <summary>
26-
/// A mechanism for CRAM-MD5.
26+
/// A mechanism for CRAM-MD5 (http://tools.ietf.org/html/draft-ietf-sasl-crammd5-10).
2727
/// </summary>
2828
internal class CramMD5Mechanism : ISaslMechanism
2929
{
@@ -60,7 +60,52 @@ public bool CanUse(MongoCredential credential)
6060
public ISaslStep Initialize(MongoConnection connection, MongoCredential credential)
6161
{
6262
return new ManagedCramMD5Implementation(credential.Username, ((PasswordEvidence)credential.Evidence).Password);
63-
//return new GsaslCramMD5Implementation(identity);
63+
}
64+
65+
// nested classes
66+
private class ManagedCramMD5Implementation : SaslImplementationBase, ISaslStep
67+
{
68+
// private fields
69+
private readonly string _username;
70+
private readonly string _password;
71+
72+
// constructors
73+
public ManagedCramMD5Implementation(string username, string password)
74+
{
75+
_username = username;
76+
_password = password;
77+
}
78+
79+
// public methods
80+
public byte[] BytesToSendToServer
81+
{
82+
get { return new byte[0]; }
83+
}
84+
85+
// public methods
86+
public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedFromServer)
87+
{
88+
var encoding = Encoding.UTF8;
89+
var mongoPassword = _username + ":mongo:" + _password;
90+
byte[] password;
91+
using (var md5 = MD5.Create())
92+
{
93+
password = GetMongoPassword(md5, encoding, _username, _password);
94+
var temp = ToHexString(password);
95+
password = encoding.GetBytes(temp);
96+
}
97+
98+
byte[] digest;
99+
using (var hmacMd5 = new HMACMD5(password))
100+
{
101+
digest = hmacMd5.ComputeHash(bytesReceivedFromServer);
102+
}
103+
104+
var response = _username + " " + ToHexString(digest);
105+
var bytesToSendToServer = encoding.GetBytes(response);
106+
107+
return new SaslCompletionStep(bytesToSendToServer);
108+
}
64109
}
65110
}
66-
}
111+
}

MongoDB.Driver/Communication/Security/Mechanisms/DigestMD5Mechanism.cs

Lines changed: 296 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
namespace MongoDB.Driver.Communication.Security.Mechanisms
2424
{
2525
/// <summary>
26-
/// A mechanism for DIGEST-MD5.
26+
/// A mechanism for DIGEST-MD5 (http://www.ietf.org/rfc/rfc2831.txt).
2727
/// </summary>
2828
internal class DigestMD5Mechanism : ISaslMechanism
2929
{
@@ -63,6 +63,299 @@ public ISaslStep Initialize(Internal.MongoConnection connection, MongoCredential
6363
connection.ServerInstance.Address.Host,
6464
credential.Username,
6565
((PasswordEvidence)credential.Evidence).Password);
66-
}
66+
}
67+
68+
// nested classes
69+
private class ManagedDigestMD5Implementation : SaslImplementationBase, ISaslStep
70+
{
71+
// private fields
72+
private readonly byte[] _cnonce;
73+
private readonly string _digestUri;
74+
private readonly string _nonceCount;
75+
private readonly string _qop;
76+
private readonly string _password;
77+
private readonly string _username;
78+
79+
// constructors
80+
public ManagedDigestMD5Implementation(string serverName, string username, string password)
81+
{
82+
_cnonce = CreateClientNonce();
83+
_digestUri = "mongodb/" + serverName;
84+
_nonceCount = "00000001";
85+
_qop = "auth";
86+
_password = password;
87+
_username = username;
88+
}
89+
90+
// public properties
91+
public byte[] BytesToSendToServer
92+
{
93+
get { return new byte[0]; }
94+
}
95+
96+
// public methods
97+
public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedFromServer)
98+
{
99+
var directives = DirectiveParser.Parse(bytesReceivedFromServer);
100+
var encoding = Encoding.UTF8;
101+
102+
var sb = new StringBuilder();
103+
sb.AppendFormat("username=\"{0}\"", _username);
104+
sb.AppendFormat(",nonce=\"{0}\"", encoding.GetString(directives["nonce"]).Replace("\"", "\\\""));
105+
sb.AppendFormat(",cnonce=\"{0}\"", encoding.GetString(_cnonce).Replace("\"", "\\\""));
106+
sb.AppendFormat(",nc={0}", _nonceCount);
107+
sb.AppendFormat(",qop={0}", _qop);
108+
sb.AppendFormat(",digest-uri=\"{0}\"", _digestUri);
109+
sb.AppendFormat(",response={0}", ComputeResponse(encoding, directives["nonce"]));
110+
sb.Append(",charset=\"utf-8\"");
111+
112+
return new ManagedDigestMD5FinalStep(encoding.GetBytes(sb.ToString()));
113+
}
114+
115+
// private methods
116+
private string ComputeResponse(Encoding encoding, byte[] nonce)
117+
{
118+
using (var md5 = MD5.Create())
119+
{
120+
var a1 = ComputeA1(encoding, md5, nonce);
121+
var a2 = ComputeA2(encoding);
122+
123+
var a1Hash = md5.ComputeHash(a1);
124+
var a2Hash = md5.ComputeHash(a2);
125+
126+
var a1Hex = ToHexString(a1Hash);
127+
var a2Hex = ToHexString(a2Hash);
128+
129+
var kd = new List<byte>();
130+
kd.AddRange(encoding.GetBytes(a1Hex));
131+
kd.Add((byte)':');
132+
kd.AddRange(nonce);
133+
kd.Add((byte)':');
134+
kd.AddRange(encoding.GetBytes(_nonceCount));
135+
kd.Add((byte)':');
136+
kd.AddRange(_cnonce);
137+
kd.Add((byte)':');
138+
kd.AddRange(encoding.GetBytes(_qop));
139+
kd.Add((byte)':');
140+
kd.AddRange(encoding.GetBytes(a2Hex));
141+
142+
var kdHash = md5.ComputeHash(kd.ToArray());
143+
return ToHexString(kdHash);
144+
}
145+
}
146+
147+
private byte[] ComputeA1(Encoding encoding, MD5 md5, byte[] nonce)
148+
{
149+
// User Token
150+
var userToken = new List<byte>();
151+
userToken.AddRange(encoding.GetBytes(_username));
152+
userToken.Add((byte)':');
153+
userToken.Add((byte)':');
154+
var passwordBytes = GetMongoPassword(md5, encoding, _username, _password);
155+
var passwordHex = ToHexString(passwordBytes);
156+
userToken.AddRange(encoding.GetBytes(passwordHex));
157+
var userTokenBytes = md5.ComputeHash(userToken.ToArray());
158+
159+
var nonceBytes = new List<byte>();
160+
nonceBytes.Add((byte)':');
161+
nonceBytes.AddRange(nonce);
162+
nonceBytes.Add((byte)':');
163+
nonceBytes.AddRange(_cnonce);
164+
165+
var result = new byte[userTokenBytes.Length + nonceBytes.Count];
166+
userTokenBytes.CopyTo(result, 0);
167+
nonceBytes.CopyTo(result, userTokenBytes.Length);
168+
169+
return result;
170+
}
171+
172+
private byte[] ComputeA2(Encoding encoding)
173+
{
174+
return encoding.GetBytes("AUTHENTICATE:" + _digestUri);
175+
}
176+
177+
private byte[] CreateClientNonce()
178+
{
179+
return Encoding.UTF8.GetBytes(new Random().Next(1234000, 99999999).ToString());
180+
}
181+
}
182+
183+
private class ManagedDigestMD5FinalStep : ISaslStep
184+
{
185+
private readonly byte[] _bytesToSendToServer;
186+
187+
public ManagedDigestMD5FinalStep(byte[] bytesToSendToServer)
188+
{
189+
_bytesToSendToServer = bytesToSendToServer;
190+
}
191+
192+
public byte[] BytesToSendToServer
193+
{
194+
get { return _bytesToSendToServer; }
195+
}
196+
197+
public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedFromServer)
198+
{
199+
return new SaslCompletionStep(new byte[0]);
200+
}
201+
}
202+
203+
private static class DirectiveParser
204+
{
205+
public static Dictionary<string, byte[]> Parse(byte[] bytes)
206+
{
207+
var s = Encoding.UTF8.GetString(bytes);
208+
209+
var parsed = new Dictionary<string, byte[]>();
210+
var index = 0;
211+
while (index < bytes.Length)
212+
{
213+
var key = ParseKey(bytes, ref index);
214+
SkipWhitespace(bytes, ref index);
215+
if (bytes[index] != '=')
216+
{
217+
throw new MongoSecurityException(string.Format("Expected a '=' after a key \"{0}\"."));
218+
}
219+
else if (key.Length == 0)
220+
{
221+
throw new MongoSecurityException("Empty directive key.");
222+
}
223+
index++; // skip =
224+
225+
var value = ParseValue(key, bytes, ref index);
226+
parsed.Add(key, value);
227+
if (index >= bytes.Length)
228+
{
229+
break;
230+
}
231+
else if (bytes[index] != ',')
232+
{
233+
throw new MongoSecurityException(string.Format("Expected a ',' after directive \"{0}\".", key));
234+
}
235+
else
236+
{
237+
index++;
238+
SkipWhitespace(bytes, ref index);
239+
}
240+
}
241+
242+
return parsed;
243+
}
244+
245+
private static string ParseKey(byte[] bytes, ref int index)
246+
{
247+
var key = new StringBuilder();
248+
while (index < bytes.Length)
249+
{
250+
var b = bytes[index];
251+
if (b == ',')
252+
{
253+
if (key.Length == 0)
254+
{
255+
// there were some extra commas, so we skip over them
256+
// and try to find the next key
257+
index++;
258+
SkipWhitespace(bytes, ref index);
259+
}
260+
else
261+
{
262+
throw new MongoSecurityException(string.Format("Directive key \"{0}\" contains a ','.", key.ToString()));
263+
}
264+
}
265+
else if (b == '=')
266+
{
267+
break;
268+
}
269+
else if (IsWhiteSpace(b))
270+
{
271+
index++;
272+
break;
273+
}
274+
else
275+
{
276+
index++;
277+
key.Append((char)b);
278+
}
279+
}
280+
281+
return key.ToString();
282+
}
283+
284+
private static byte[] ParseValue(string key, byte[] bytes, ref int index)
285+
{
286+
List<byte> value = new List<byte>();
287+
bool isQuoted = false;
288+
while (index < bytes.Length)
289+
{
290+
var b = bytes[index];
291+
if (b == '\\')
292+
{
293+
index++; // skip escape
294+
if (index < bytes.Length)
295+
{
296+
value.Add(b);
297+
index++;
298+
}
299+
else
300+
{
301+
throw new MongoSecurityException(string.Format("Unmatched quote found in value of directive key \"{0}\".", key));
302+
}
303+
}
304+
else if (b == '"')
305+
{
306+
index++;
307+
if (isQuoted)
308+
{
309+
// we have closed the quote...
310+
break;
311+
}
312+
else if (value.Count == 0)
313+
{
314+
isQuoted = true;
315+
}
316+
else
317+
{
318+
// quote in the middle of an unquoted string
319+
value.Add(b);
320+
}
321+
}
322+
else
323+
{
324+
if (b == ',' && !isQuoted)
325+
{
326+
break;
327+
}
328+
value.Add(b);
329+
index++;
330+
}
331+
}
332+
333+
return value.ToArray();
334+
}
335+
336+
private static bool IsWhiteSpace(byte b)
337+
{
338+
switch (b)
339+
{
340+
case 13: // US-ASCII CR, carriage return
341+
case 10: // US-ASCII LF, linefeed
342+
case 32: // US-ASCII SP, space
343+
case 9: // US-ASCII HT, horizontal-tab
344+
return true;
345+
}
346+
return false;
347+
}
348+
349+
private static void SkipWhitespace(byte[] bytes, ref int index)
350+
{
351+
for (; index < bytes.Length; index++)
352+
{
353+
if (!IsWhiteSpace(bytes[index]))
354+
{
355+
return;
356+
}
357+
}
358+
}
359+
}
67360
}
68-
}
361+
}

0 commit comments

Comments
 (0)