Packages
postgrex
0.21.0
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.13
0.15.12
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.13.0-rc.0
0.12.2
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.2
PostgreSQL driver for Elixir
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/postgrex/scram.ex
defmodule Postgrex.SCRAM do
@moduledoc false
alias Postgrex.SCRAM
@hash_length 32
@nonce_length 24
@nonce_rand_bytes div(@nonce_length * 6, 8)
@nonce_prefix "n,,n=,r="
@nonce_encoded_size <<byte_size(@nonce_prefix) + @nonce_length::signed-size(32)>>
def client_first do
nonce = @nonce_rand_bytes |> :crypto.strong_rand_bytes() |> Base.encode64()
["SCRAM-SHA-256", 0, @nonce_encoded_size, @nonce_prefix, nonce]
end
def client_final(data, opts) do
# Extract data from server-first message
server = parse_server_data(data)
{:ok, server_s} = Base.decode64(server[?s])
server_i = String.to_integer(server[?i])
# Create and cache client and server keys if they don't already exist
pass = Keyword.fetch!(opts, :password)
cache_key = create_cache_key(pass, server_s, server_i)
{client_key, _server_key} =
SCRAM.LockedCache.run(cache_key, fn ->
calculate_client_server_keys(pass, server_s, server_i)
end)
# Construct client signature and proof
message_without_proof = ["c=biws,r=", server[?r]]
client_nonce = binary_part(server[?r], 0, @nonce_length)
message = ["n=,r=", client_nonce, ",r=", server[?r], ",s=", server[?s], ",i=", server[?i], ?,]
auth_message = IO.iodata_to_binary([message | message_without_proof])
client_sig = hmac(:sha256, :crypto.hash(:sha256, client_key), auth_message)
proof = Base.encode64(:crypto.exor(client_key, client_sig))
# Store data needed to verify the server signature
scram_state = %{salt: server_s, iterations: server_i, auth_message: auth_message}
{[message_without_proof, ",p=", proof], scram_state}
end
def verify_server(data, scram_state, opts) do
data
|> parse_server_data()
|> do_verify_server(scram_state, opts)
end
defp do_verify_server(%{?e => server_e}, _scram_state, _opts) do
msg = "error received in SCRAM server final message: #{inspect(server_e)}"
{:error, %Postgrex.Error{message: msg}}
end
defp do_verify_server(%{?v => server_v}, scram_state, opts) do
# Decode server signature from the server-final message
{:ok, server_sig} = Base.decode64(server_v)
# Construct expected server signature
pass = Keyword.fetch!(opts, :password)
cache_key = create_cache_key(pass, scram_state.salt, scram_state.iterations)
{_client_key, server_key} = SCRAM.LockedCache.get(cache_key)
expected_server_sig = hmac(:sha256, server_key, scram_state.auth_message)
# Verify the server signature sent to us is correct
if expected_server_sig == server_sig do
:ok
else
msg = "cannot verify SCRAM server signature"
{:error, %Postgrex.Error{message: msg}}
end
end
defp do_verify_server(server, _scram_state, _opts) do
msg = "unsupported SCRAM server final message: #{inspect(server)}"
{:error, %Postgrex.Error{message: msg}}
end
defp parse_server_data(data) do
for kv <- :binary.split(data, ",", [:global]), into: %{} do
<<k, "=", v::binary>> = kv
{k, v}
end
end
defp create_cache_key(pass, salt, iterations) do
{:crypto.hash(:sha256, pass), salt, iterations}
end
defp calculate_client_server_keys(pass, salt, iterations) do
salted_pass = hash_password(pass, salt, iterations)
client_key = hmac(:sha256, salted_pass, "Client Key")
server_key = hmac(:sha256, salted_pass, "Server Key")
{client_key, server_key}
end
defp hash_password(secret, salt, iterations) do
hash_password(secret, salt, iterations, 1, [], 0)
end
defp hash_password(_secret, _salt, _iterations, _block_index, acc, length)
when length >= @hash_length do
acc
|> IO.iodata_to_binary()
|> binary_part(0, @hash_length)
end
defp hash_password(secret, salt, iterations, block_index, acc, length) do
initial = hmac(:sha256, secret, <<salt::binary, block_index::integer-size(32)>>)
block = iterate(secret, iterations - 1, initial, initial)
length = byte_size(block) + length
hash_password(secret, salt, iterations, block_index + 1, [acc | block], length)
end
defp iterate(_secret, 0, _prev, acc), do: acc
defp iterate(secret, iteration, prev, acc) do
next = hmac(:sha256, secret, prev)
iterate(secret, iteration - 1, next, :crypto.exor(next, acc))
end
# :crypto.mac/4 was added in OTP-22.1, and :crypto.hmac/3 removed in OTP-24.
# Check which function to use at compile time to avoid doing a round-trip
# to the code server on every call. The downside is this module won't work
# if it's compiled on OTP-22.0 or older then executed on OTP-24 or newer.
if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do
defp hmac(type, key, data), do: :crypto.mac(:hmac, type, key, data)
else
defp hmac(type, key, data), do: :crypto.hmac(type, key, data)
end
end