epgsql_scram.erl 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. %%% coding: utf-8
  2. %%% @doc
  3. %%% SCRAM--SHA-256 helper functions
  4. %%%
  5. %%% <ul>
  6. %%% <li>[https://www.postgresql.org/docs/current/static/sasl-authentication.html]</li>
  7. %%% <li>[https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism]</li>
  8. %%% <li>[https://tools.ietf.org/html/rfc7677]</li>
  9. %%% <li>[https://tools.ietf.org/html/rfc5802]</li>
  10. %%% </ul>
  11. %%% @end
  12. -module(epgsql_scram).
  13. -export([get_nonce/1,
  14. get_client_first/2,
  15. get_client_final/4,
  16. parse_server_first/2,
  17. parse_server_final/1]).
  18. -export([hi/3,
  19. hmac/2,
  20. h/1,
  21. bin_xor/2]).
  22. -type nonce() :: binary().
  23. -type server_first() :: [{nonce, nonce()} |
  24. {salt, binary()} |
  25. {i, pos_integer()} |
  26. {raw, binary()}].
  27. -spec get_client_first(iodata(), nonce()) -> iodata().
  28. get_client_first(UserName, Nonce) ->
  29. %% Username is ignored by postgresql
  30. [<<"n,,">> | client_first_bare(UserName, Nonce)].
  31. client_first_bare(UserName, Nonce) ->
  32. [<<"n=">>, UserName, <<",r=">>, Nonce].
  33. %% @doc Generate unique ASCII string.
  34. %% Resulting string length isn't guaranteed, but it's guaranteed to be unique and will
  35. %% contain `NumRandomBytes' of random data.
  36. -spec get_nonce(pos_integer()) -> nonce().
  37. get_nonce(NumRandomBytes) when NumRandomBytes < 255 ->
  38. Random = crypto:strong_rand_bytes(NumRandomBytes),
  39. Unique = binary:encode_unsigned(unique()),
  40. NonceBin = <<NumRandomBytes, Random:NumRandomBytes/binary, Unique/binary>>,
  41. base64:encode(NonceBin).
  42. -spec parse_server_first(binary(), nonce()) -> server_first().
  43. parse_server_first(ServerFirst, ClientNonce) ->
  44. PartsB = binary:split(ServerFirst, <<",">>, [global]),
  45. (length(PartsB) == 3) orelse error({invalid_server_first, ServerFirst}),
  46. Parts =
  47. lists:map(
  48. fun(<<"r=", R/binary>>) ->
  49. {nonce, R};
  50. (<<"s=", S/binary>>) ->
  51. {salt, base64:decode(S)};
  52. (<<"i=", I/binary>>) ->
  53. {i, binary_to_integer(I)}
  54. end, PartsB),
  55. check_nonce(ClientNonce, proplists:get_value(nonce, Parts)),
  56. [{raw, ServerFirst} | Parts].
  57. %% SaltedPassword := Hi(Normalize(password), salt, i)
  58. %% ClientKey := HMAC(SaltedPassword, "Client Key")
  59. %% StoredKey := H(ClientKey)
  60. %% AuthMessage := client-first-message-bare + "," + server-first-message + "," + client-final-message-without-proof
  61. %% ClientSignature := HMAC(StoredKey, AuthMessage)
  62. %% ClientProof := ClientKey XOR ClientSignature
  63. -spec get_client_final(server_first(), nonce(), iodata(), iodata()) ->
  64. {ClientFinal :: iodata(), ServerSignature :: binary()}.
  65. get_client_final(SrvFirst, ClientNonce, UserName, Password) ->
  66. ChannelBinding = <<"c=biws">>, %channel-binding isn't implemented
  67. Nonce = [<<"r=">>, proplists:get_value(nonce, SrvFirst)],
  68. Salt = proplists:get_value(salt, SrvFirst),
  69. I = proplists:get_value(i, SrvFirst),
  70. SaltedPassword = hi(normalize(Password), Salt, I),
  71. ClientKey = hmac(SaltedPassword, "Client Key"),
  72. StoredKey = h(ClientKey),
  73. ClientFirstBare = client_first_bare(UserName, ClientNonce),
  74. ServerFirst = proplists:get_value(raw, SrvFirst),
  75. ClientFinalWithoutProof = [ChannelBinding, ",", Nonce],
  76. AuthMessage = [ClientFirstBare, ",", ServerFirst, ",", ClientFinalWithoutProof],
  77. ClientSignature = hmac(StoredKey, AuthMessage),
  78. ClientProof = bin_xor(ClientKey, ClientSignature),
  79. ServerKey = hmac(SaltedPassword, "Server Key"),
  80. ServerSignature = hmac(ServerKey, AuthMessage),
  81. {[ClientFinalWithoutProof, ",p=", base64:encode(ClientProof)], ServerSignature}.
  82. -spec parse_server_final(binary()) -> {ok, binary()} | {error, binary()}.
  83. parse_server_final(<<"v=", ServerFinal/binary>>) ->
  84. [ServerFinal1 | _] = binary:split(ServerFinal, <<",">>),
  85. {ok, base64:decode(ServerFinal1)};
  86. parse_server_final(<<"e=", ServerError/binary>>) ->
  87. {error, ServerError}.
  88. %% Helpers
  89. %% TODO: implement according to rfc3454
  90. normalize(Str) ->
  91. lists:all(fun is_ascii_non_control/1, unicode:characters_to_list(Str, utf8))
  92. orelse error({scram_non_ascii_password, Str}),
  93. Str.
  94. is_ascii_non_control(C) when C > 16#1F, C < 16#7F -> true;
  95. is_ascii_non_control(_) -> false.
  96. check_nonce(ClientNonce, ServerNonce) ->
  97. Size = size(ClientNonce),
  98. <<ClientNonce:Size/binary, _/binary>> = ServerNonce,
  99. true.
  100. hi(Str, Salt, I) ->
  101. U1 = hmac(Str, <<Salt/binary, 1:32/integer-big>>),
  102. hi1(Str, U1, U1, I - 1).
  103. hi1(_Str, _U, Hi, 0) ->
  104. Hi;
  105. hi1(Str, U, Hi, I) ->
  106. U2 = hmac(Str, U),
  107. Hi1 = bin_xor(Hi, U2),
  108. hi1(Str, U2, Hi1, I - 1).
  109. hmac(Key, Str) ->
  110. crypto:hmac(sha256, Key, Str).
  111. h(Str) ->
  112. crypto:hash(sha256, Str).
  113. %% word 'xor' is reserved
  114. bin_xor(B1, B2) ->
  115. crypto:exor(B1, B2).
  116. unique() ->
  117. erlang:unique_integer([positive]).
  118. -ifdef(TEST).
  119. -include_lib("eunit/include/eunit.hrl").
  120. exchange_test() ->
  121. Password = <<"foobar">>,
  122. Nonce = <<"9IZ2O01zb9IgiIZ1WJ/zgpJB">>,
  123. Username = <<>>,
  124. ClientFirst = <<"n,,n=,r=9IZ2O01zb9IgiIZ1WJ/zgpJB">>,
  125. ServerFirst = <<"r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,s=fs3IXBy7U7+IvVjZ,i=4096">>,
  126. ClientFinal = <<"c=biws,r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,p=AmNKosjJzS31NTlQ"
  127. "YNs5BTeQjdHdk7lOflDo5re2an8=">>,
  128. ServerFinal = <<"v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw=">>,
  129. ?assertEqual(ClientFirst, iolist_to_binary(get_client_first(Username, Nonce))),
  130. SF = parse_server_first(ServerFirst, Nonce),
  131. {CF, ServerProof} = get_client_final(SF, Nonce, Username, Password),
  132. ?assertEqual(ClientFinal, iolist_to_binary(CF)),
  133. ?assertEqual({ok, ServerProof}, parse_server_final(ServerFinal)).
  134. normalize_test() ->
  135. ?assertEqual(<<"123 !~">>, normalize(<<"123 !~">>)),
  136. ?assertError({scram_non_ascii_password, _}, normalize(<<"привет"/utf8>>)).
  137. -endif.