Skip to content

Commit

Permalink
Merge pull request #1632 from ngurrola/mongo_scram
Browse files Browse the repository at this point in the history
Implemented SCRAM-SHA1 authentication method for MongoDB. Fixes #1243. Fixes #1557.
  • Loading branch information
s-ludwig committed Dec 19, 2016
1 parent 761adf6 commit 19acf2a
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 1 deletion.
153 changes: 153 additions & 0 deletions mongodb/vibe/db/mongo/sasl.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
SASL authentication functions
Copyright: © 2012-2016 Nicolas Gurrola
License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
Authors: Nicolas Gurrola
*/
module vibe.db.mongo.sasl;

import std.algorithm;
import std.base64;
import std.conv;
import std.digest.hmac;
import std.digest.sha;
import std.exception;
import std.format;
import std.string;
import std.utf;
import vibe.crypto.cryptorand;

private SHA1HashMixerRNG g_rng;

package struct ScramState
{
private string m_firstMessageBare;
private string m_nonce;
private DigestType!SHA1 m_saltedPassword;
private string m_authMessage;

string createInitialRequest(string user)
{
ubyte[18] randomBytes;
g_rng.read(randomBytes[]);
m_nonce = Base64.encode(randomBytes);

m_firstMessageBare = format("n=%s,r=%s", escapeUsername(user), m_nonce);
return format("n,,%s", m_firstMessageBare);
}

string update(string password, string challenge)
{
string serverFirstMessage = challenge;

string next = challenge.find(',');
if (challenge.length < 2 || challenge[0 .. 2] != "r=" || next.length < 3 || next[1 .. 3] != "s=")
throw new Exception("Invalid server challenge format");
string serverNonce = challenge[2 .. $ - next.length];
challenge = next[3 .. $];
next = challenge.find(',');
ubyte[] salt = Base64.decode(challenge[0 .. $ - next.length]);

if (next.length < 3 || next[1 .. 3] != "i=")
throw new Exception("Invalid server challenge format");
int iterations = next[3 .. $].to!int();

if (serverNonce[0 .. m_nonce.length] != m_nonce)
throw new Exception("Invalid server nonce received");
string finalMessage = format("c=biws,r=%s", serverNonce);

m_saltedPassword = pbkdf2(password.representation, salt, iterations);
m_authMessage = format("%s,%s,%s", m_firstMessageBare, serverFirstMessage, finalMessage);

auto proof = getClientProof(m_saltedPassword, m_authMessage);
return format("%s,p=%s", finalMessage, Base64.encode(proof));
}

string finalize(string challenge)
{
if (challenge.length < 2 || challenge[0 .. 2] != "v=")
{
throw new Exception("Invalid server signature format");
}
if (!verifyServerSignature(Base64.decode(challenge[2 .. $]), m_saltedPassword, m_authMessage))
{
throw new Exception("Invalid server signature");
}
return null;
}

private static string escapeUsername(string user)
{
char[] buffer;
foreach (i, dchar ch; user)
{
if (ch == ',' || ch == '=') {
if (!buffer) {
buffer.reserve(user.length + 2);
buffer ~= user[0 .. i];
}
if (ch == ',')
buffer ~= "=2C";
else
buffer ~= "=3D";
} else if (buffer)
encode(buffer, ch);
}
return buffer ? assumeUnique(buffer) : user;
}

unittest
{
string user = "user";
assert(escapeUsername(user) == user);
assert(escapeUsername(user) is user);
assert(escapeUsername("user,1") == "user=2C1");
assert(escapeUsername("user=1") == "user=3D1");
assert(escapeUsername("u,=ser1") == "u=2C=3Dser1");
assert(escapeUsername("u=se=r1") == "u=3Dse=3Dr1");
}

private static auto getClientProof(DigestType!SHA1 saltedPassword, string authMessage)
{
auto clientKey = hmac!SHA1("Client Key".representation, saltedPassword);
auto storedKey = sha1Of(clientKey);
auto clientSignature = hmac!SHA1(authMessage.representation, storedKey);

foreach (i; 0 .. clientKey.length)
{
clientKey[i] = clientKey[i] ^ clientSignature[i];
}
return clientKey;
}

private static bool verifyServerSignature(ubyte[] signature, DigestType!SHA1 saltedPassword, string authMessage)
{
auto serverKey = hmac!SHA1("Server Key".representation, saltedPassword);
auto serverSignature = hmac!SHA1(authMessage.representation, serverKey);
return serverSignature == signature;
}
}

private DigestType!SHA1 pbkdf2(const ubyte[] password, const ubyte[] salt, int iterations)
{
import std.bitmanip;

ubyte[4] intBytes = [0, 0, 0, 1];
auto last = hmac!SHA1(salt, intBytes[], password);
auto current = last;
foreach (i; 1 .. iterations)
{
last = hmac!SHA1(last[], password);
foreach (j; 0 .. current.length)
{
current[j] = current[j] ^ last[j];
}
}
return current;
}

static this()
{
g_rng = new SHA1HashMixerRNG();
}
63 changes: 62 additions & 1 deletion source/vibe/db/mongo/connection.d
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ final class MongoConnection {
m_bytesRead = 0;
if(m_settings.digest != string.init)
{
authenticate();
if (m_settings.authMechanism == MongoAuthMechanism.scramSHA1)
scramAuthenticate();
else
authenticate();

}
else if (m_settings.sslPEMKeyFile != null && m_settings.username != null)
{
Expand Down Expand Up @@ -490,6 +494,63 @@ final class MongoConnection {
}
);
}

private void scramAuthenticate()
{
import vibe.db.mongo.sasl;
string cn = (m_settings.database == string.init ? "admin" : m_settings.database) ~ ".$cmd";

ScramState state;
string payload = state.createInitialRequest(m_settings.username);

auto cmd = Bson.emptyObject;
cmd["saslStart"] = Bson(1);
cmd["mechanism"] = Bson("SCRAM-SHA-1");
cmd["payload"] = Bson(BsonBinData(BsonBinData.Type.generic, payload.representation));
string response;
Bson conversationId;
query!Bson(cn, QueryFlags.None, 0, -1, cmd, Bson(null),
(cursor, flags, first_doc, num_docs) {
if ((flags & ReplyFlags.QueryFailure) || num_docs != 1)
throw new MongoDriverException("SASL start failed.");
},
(idx, ref doc) {
if (doc["ok"].get!double != 1.0)
throw new MongoAuthException("Authentication failed.");
response = cast(string)doc["payload"].get!BsonBinData().rawData;
conversationId = doc["conversationId"];
});
payload = state.update(m_settings.digest, response);
cmd = Bson.emptyObject;
cmd["saslContinue"] = Bson(1);
cmd["conversationId"] = conversationId;
cmd["payload"] = Bson(BsonBinData(BsonBinData.Type.generic, payload.representation));
query!Bson(cn, QueryFlags.None, 0, -1, cmd, Bson(null),
(cursor, flags, first_doc, num_docs) {
if ((flags & ReplyFlags.QueryFailure) || num_docs != 1)
throw new MongoDriverException("SASL continue failed.");
},
(idx, ref doc) {
if (doc["ok"].get!double != 1.0)
throw new MongoAuthException("Authentication failed.");
response = cast(string)doc["payload"].get!BsonBinData().rawData;
});

payload = state.finalize(response);
cmd = Bson.emptyObject;
cmd["saslContinue"] = Bson(1);
cmd["conversationId"] = conversationId;
cmd["payload"] = Bson(BsonBinData(BsonBinData.Type.generic, payload.representation));
query!Bson(cn, QueryFlags.None, 0, -1, cmd, Bson(null),
(cursor, flags, first_doc, num_docs) {
if ((flags & ReplyFlags.QueryFailure) || num_docs != 1)
throw new MongoDriverException("SASL finish failed.");
},
(idx, ref doc) {
if (doc["ok"].get!double != 1.0)
throw new MongoAuthException("Authentication failed.");
});
}
}

private enum OpCode : int {
Expand Down
20 changes: 20 additions & 0 deletions source/vibe/db/mongo/settings.d
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ bool parseMongoDBUrl(out MongoClientSettings cfg, string url)
case "sockettimeoutms": setLong(cfg.socketTimeoutMS); warnNotImplemented(); break;
case "ssl": setBool(cfg.ssl); break;
case "sslverifycertificate": setBool(cfg.sslverifycertificate); break;
case "authmechanism": cfg.authMechanism = parseAuthMechanism(value); break;
case "wtimeoutms": setLong(cfg.wTimeoutMS); break;
case "w":
try {
Expand Down Expand Up @@ -296,6 +297,24 @@ unittest
assert(cfg.hosts[0].port == 27017);
}

enum MongoAuthMechanism
{
none,
scramSHA1,
mongoDBCR,
mongoDBX509
}

private MongoAuthMechanism parseAuthMechanism(string str)
{
switch (str) {
case "SCRAM-SHA-1": return MongoAuthMechanism.scramSHA1;
case "MONGODB-CR": return MongoAuthMechanism.mongoDBCR;
case "MONGODB-X509": return MongoAuthMechanism.mongoDBX509;
default: throw new Exception("Auth mechanism \"" ~ str ~ "\" not supported");
}
}

class MongoClientSettings
{
enum ushort defaultPort = 27017;
Expand All @@ -317,6 +336,7 @@ class MongoClientSettings
bool sslverifycertificate = true;
string sslPEMKeyFile;
string sslCAFile;
MongoAuthMechanism authMechanism;

static string makeDigest(string username, string password)
{
Expand Down

0 comments on commit 19acf2a

Please sign in to comment.