Packages

MongoDB driver for Elixir

Retired package: Deprecated - Use mongodb_driver instead, see https://github.com/elixir-mongo/mongodb_ecto/tree/1cc91b087#migrating-to-20 for migration advice

Current section

Files

Jump to
mongodb lib mongo auth scram.ex
Raw

lib/mongo/auth/scram.ex

defmodule Mongo.Auth.SCRAM do
@moduledoc false
import Mongo.BinaryUtils
import Mongo.Protocol.Utils
import Bitwise
def auth({username, password}, s) do
# TODO: Wrap and log error
nonce = nonce()
first_bare = first_bare(username, nonce)
payload = first_message(first_bare)
message = [saslStart: 1, mechanism: "SCRAM-SHA-1", payload: payload]
result =
with {:ok, %{"ok" => ok} = reply} when ok == 1 <- command(-2, message, s),
{message, signature} = first(reply, first_bare, username, password, nonce),
{:ok, %{"ok" => ok} = reply} when ok == 1 <- command(-3, message, s),
message = second(reply, signature),
{:ok, %{"ok" => ok} = reply} when ok == 1 <- command(-4, message, s),
do: final(reply)
case result do
:ok ->
:ok
{:ok, %{"ok" => z, "errmsg" => reason, "code" => code}} when z == 0 ->
{:error,
Mongo.Error.exception(message: "auth failed for user #{username}: #{reason}", code: code)}
error ->
error
end
end
defp first(
%{"conversationId" => 1, "payload" => server_payload, "done" => false},
first_bare,
username,
password,
client_nonce
) do
params = parse_payload(server_payload)
server_nonce = params["r"]
salt = Base.decode64!(params["s"])
iter = String.to_integer(params["i"])
pass = digest_password(username, password)
salted_password = hi(pass, salt, iter)
<<^client_nonce::binary(24), _::binary>> = server_nonce
client_message = "c=biws,r=#{server_nonce}"
auth_message = "#{first_bare},#{server_payload.binary},#{client_message}"
server_signature = generate_signature(salted_password, auth_message)
proof = generate_proof(salted_password, auth_message)
client_final_message = %BSON.Binary{binary: "#{client_message},#{proof}"}
message = [saslContinue: 1, conversationId: 1, payload: client_final_message]
{message, server_signature}
end
defp second(%{"conversationId" => 1, "payload" => payload, "done" => false}, signature) do
params = parse_payload(payload)
^signature = Base.decode64!(params["v"])
[saslContinue: 1, conversationId: 1, payload: %BSON.Binary{binary: ""}]
end
defp final(%{"conversationId" => 1, "payload" => %BSON.Binary{binary: ""}, "done" => true}) do
:ok
end
defp first_message(first_bare) do
%BSON.Binary{binary: "n,,#{first_bare}"}
end
defp first_bare(username, nonce) do
"n=#{encode_username(username)},r=#{nonce}"
end
defp hi(password, salt, iterations) do
Mongo.PBKDF2Cache.pbkdf2(password, salt, iterations)
end
defp generate_proof(salted_password, auth_message) do
client_key = mac_fun(:sha, salted_password, "Client Key")
stored_key = :crypto.hash(:sha, client_key)
signature = mac_fun(:sha, stored_key, auth_message)
client_proof = xor_keys(client_key, signature, "")
"p=#{Base.encode64(client_proof)}"
end
defp generate_signature(salted_password, auth_message) do
server_key = mac_fun(:sha, salted_password, "Server Key")
mac_fun(:sha, server_key, auth_message)
end
if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do
defp mac_fun(digest, secret, data), do: :crypto.mac(:hmac, digest, secret, data)
else
defp mac_fun(digest, secret, data), do: :crypto.hmac(digest, secret, data)
end
defp xor_keys("", "", result),
do: result
defp xor_keys(<<fa, ra::binary>>, <<fb, rb::binary>>, result),
do: xor_keys(ra, rb, <<result::binary, bxor(fa, fb)>>)
defp nonce do
18
|> :crypto.strong_rand_bytes()
|> Base.encode64()
end
defp encode_username(username) do
username
|> String.replace("=", "=3D")
|> String.replace(",", "=2C")
end
defp parse_payload(%BSON.Binary{subtype: :generic, binary: payload}) do
payload
|> String.split(",")
|> Enum.into(%{}, &List.to_tuple(String.split(&1, "=", parts: 2)))
end
end