2011-08-16 00:25:40 +02:00
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%%% File : cyrsasl_scram.erl
|
|
|
|
%%% Author : Stephen Röttger <stephen.roettger@googlemail.com>
|
|
|
|
%%% Purpose : SASL SCRAM authentication
|
|
|
|
%%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com>
|
|
|
|
%%%
|
|
|
|
%%%
|
2015-01-08 17:34:43 +01:00
|
|
|
%%% ejabberd, Copyright (C) 2002-2015 ProcessOne
|
2011-08-16 00:25:40 +02:00
|
|
|
%%%
|
|
|
|
%%% This program is free software; you can redistribute it and/or
|
|
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
|
|
%%% License, or (at your option) any later version.
|
|
|
|
%%%
|
|
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
%%% General Public License for more details.
|
|
|
|
%%%
|
2014-02-22 11:27:40 +01:00
|
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
2011-08-16 00:25:40 +02:00
|
|
|
%%%
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
|
2011-08-16 00:25:03 +02:00
|
|
|
-module(cyrsasl_scram).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2011-08-16 00:25:03 +02:00
|
|
|
-author('stephen.roettger@googlemail.com').
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-export([start/1, stop/0, mech_new/4, mech_step/2]).
|
2011-08-16 00:25:03 +02:00
|
|
|
|
|
|
|
-include("ejabberd.hrl").
|
2013-04-08 11:12:54 +02:00
|
|
|
-include("logger.hrl").
|
2011-08-16 00:25:03 +02:00
|
|
|
|
2013-04-17 12:34:53 +02:00
|
|
|
-include("jlib.hrl").
|
|
|
|
|
2011-08-16 00:25:03 +02:00
|
|
|
-behaviour(cyrsasl).
|
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
-record(state,
|
|
|
|
{step = 2 :: 2 | 4,
|
|
|
|
stored_key = <<"">> :: binary(),
|
|
|
|
server_key = <<"">> :: binary(),
|
|
|
|
username = <<"">> :: binary(),
|
|
|
|
get_password :: fun(),
|
|
|
|
check_password :: fun(),
|
|
|
|
auth_message = <<"">> :: binary(),
|
|
|
|
client_nonce = <<"">> :: binary(),
|
|
|
|
server_nonce = <<"">> :: binary()}).
|
2011-08-16 00:25:03 +02:00
|
|
|
|
|
|
|
-define(SALT_LENGTH, 16).
|
2013-03-14 10:33:02 +01:00
|
|
|
|
2011-08-16 00:25:03 +02:00
|
|
|
-define(NONCE_LENGTH, 16).
|
|
|
|
|
|
|
|
start(_Opts) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
cyrsasl:register_mechanism(<<"SCRAM-SHA-1">>, ?MODULE,
|
|
|
|
scram).
|
2011-08-16 00:25:03 +02:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
stop() -> ok.
|
2011-08-16 00:25:03 +02:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
mech_new(_Host, GetPassword, _CheckPassword,
|
|
|
|
_CheckPasswordDigest) ->
|
2011-08-16 00:25:03 +02:00
|
|
|
{ok, #state{step = 2, get_password = GetPassword}}.
|
|
|
|
|
|
|
|
mech_step(#state{step = 2} = State, ClientIn) ->
|
2013-04-17 12:34:53 +02:00
|
|
|
case re:split(ClientIn, <<",">>, [{return, binary}]) of
|
|
|
|
[_CBind, _AuthorizationIdentity, _UserNameAttribute, _ClientNonceAttribute, ExtensionAttribute | _]
|
|
|
|
when ExtensionAttribute /= [] ->
|
|
|
|
{error, <<"protocol-error-extension-not-supported">>};
|
|
|
|
[CBind, _AuthorizationIdentity, UserNameAttribute, ClientNonceAttribute | _]
|
2013-03-14 10:33:02 +01:00
|
|
|
when (CBind == <<"y">>) or (CBind == <<"n">>) ->
|
|
|
|
case parse_attribute(UserNameAttribute) of
|
|
|
|
{error, Reason} -> {error, Reason};
|
|
|
|
{_, EscapedUserName} ->
|
|
|
|
case unescape_username(EscapedUserName) of
|
|
|
|
error -> {error, <<"protocol-error-bad-username">>};
|
|
|
|
UserName ->
|
|
|
|
case parse_attribute(ClientNonceAttribute) of
|
|
|
|
{$r, ClientNonce} ->
|
2015-01-08 06:12:05 +01:00
|
|
|
{Ret, _AuthModule} = (State#state.get_password)(UserName),
|
|
|
|
case {Ret, jlib:resourceprep(Ret)} of
|
2013-03-14 10:33:02 +01:00
|
|
|
{false, _} -> {error, <<"not-authorized">>, UserName};
|
2015-01-19 08:00:18 +01:00
|
|
|
{_, error} when is_binary(Ret) -> ?WARNING_MSG("invalid plain password", []), {error, <<"not-authorized">>, UserName};
|
2015-01-08 06:12:05 +01:00
|
|
|
{Ret, _} ->
|
2013-03-14 10:33:02 +01:00
|
|
|
{StoredKey, ServerKey, Salt, IterationCount} =
|
|
|
|
if is_tuple(Ret) -> Ret;
|
|
|
|
true ->
|
|
|
|
TempSalt =
|
|
|
|
crypto:rand_bytes(?SALT_LENGTH),
|
|
|
|
SaltedPassword =
|
|
|
|
scram:salted_password(Ret,
|
|
|
|
TempSalt,
|
|
|
|
?SCRAM_DEFAULT_ITERATION_COUNT),
|
|
|
|
{scram:stored_key(scram:client_key(SaltedPassword)),
|
|
|
|
scram:server_key(SaltedPassword),
|
|
|
|
TempSalt,
|
|
|
|
?SCRAM_DEFAULT_ITERATION_COUNT}
|
|
|
|
end,
|
|
|
|
ClientFirstMessageBare =
|
|
|
|
str:substr(ClientIn,
|
|
|
|
str:str(ClientIn, <<"n=">>)),
|
|
|
|
ServerNonce =
|
|
|
|
jlib:encode_base64(crypto:rand_bytes(?NONCE_LENGTH)),
|
|
|
|
ServerFirstMessage =
|
|
|
|
iolist_to_binary(
|
|
|
|
["r=",
|
|
|
|
ClientNonce,
|
|
|
|
ServerNonce,
|
|
|
|
",", "s=",
|
|
|
|
jlib:encode_base64(Salt),
|
|
|
|
",", "i=",
|
|
|
|
integer_to_list(IterationCount)]),
|
|
|
|
{continue, ServerFirstMessage,
|
|
|
|
State#state{step = 4, stored_key = StoredKey,
|
|
|
|
server_key = ServerKey,
|
|
|
|
auth_message =
|
|
|
|
<<ClientFirstMessageBare/binary,
|
|
|
|
",", ServerFirstMessage/binary>>,
|
|
|
|
client_nonce = ClientNonce,
|
|
|
|
server_nonce = ServerNonce,
|
|
|
|
username = UserName}}
|
|
|
|
end;
|
|
|
|
_Else -> {error, <<"not-supported">>}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end;
|
|
|
|
_Else -> {error, <<"bad-protocol">>}
|
|
|
|
end;
|
2011-08-16 00:25:03 +02:00
|
|
|
mech_step(#state{step = 4} = State, ClientIn) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
case str:tokens(ClientIn, <<",">>) of
|
|
|
|
[GS2ChannelBindingAttribute, NonceAttribute,
|
|
|
|
ClientProofAttribute] ->
|
|
|
|
case parse_attribute(GS2ChannelBindingAttribute) of
|
2013-04-17 12:34:53 +02:00
|
|
|
{$c, CVal} ->
|
|
|
|
ChannelBindingSupport = binary:at(jlib:decode_base64(CVal), 0),
|
|
|
|
if (ChannelBindingSupport == $n)
|
|
|
|
or (ChannelBindingSupport == $y) ->
|
|
|
|
Nonce = <<(State#state.client_nonce)/binary,
|
|
|
|
(State#state.server_nonce)/binary>>,
|
|
|
|
case parse_attribute(NonceAttribute) of
|
|
|
|
{$r, CompareNonce} when CompareNonce == Nonce ->
|
|
|
|
case parse_attribute(ClientProofAttribute) of
|
|
|
|
{$p, ClientProofB64} ->
|
|
|
|
ClientProof = jlib:decode_base64(ClientProofB64),
|
|
|
|
AuthMessage = iolist_to_binary(
|
|
|
|
[State#state.auth_message,
|
|
|
|
",",
|
|
|
|
str:substr(ClientIn, 1,
|
|
|
|
str:str(ClientIn, <<",p=">>)
|
|
|
|
- 1)]),
|
|
|
|
ClientSignature =
|
|
|
|
scram:client_signature(State#state.stored_key,
|
|
|
|
AuthMessage),
|
|
|
|
ClientKey = scram:client_key(ClientProof,
|
|
|
|
ClientSignature),
|
|
|
|
CompareStoredKey = scram:stored_key(ClientKey),
|
|
|
|
if CompareStoredKey == State#state.stored_key ->
|
|
|
|
ServerSignature =
|
|
|
|
scram:server_signature(State#state.server_key,
|
|
|
|
AuthMessage),
|
|
|
|
{ok, [{username, State#state.username}],
|
|
|
|
<<"v=",
|
|
|
|
(jlib:encode_base64(ServerSignature))/binary>>};
|
2015-04-17 20:21:11 +02:00
|
|
|
true -> {error, <<"bad-auth">>, State#state.username}
|
2013-04-17 12:34:53 +02:00
|
|
|
end;
|
|
|
|
_Else -> {error, <<"bad-protocol">>}
|
2013-03-14 10:33:02 +01:00
|
|
|
end;
|
2013-04-17 12:34:53 +02:00
|
|
|
{$r, _} -> {error, <<"bad-nonce">>};
|
2013-03-14 10:33:02 +01:00
|
|
|
_Else -> {error, <<"bad-protocol">>}
|
2013-04-17 12:34:53 +02:00
|
|
|
end;
|
|
|
|
true -> {error, <<"bad-channel-binding">>}
|
2011-08-16 00:25:03 +02:00
|
|
|
end;
|
2013-03-14 10:33:02 +01:00
|
|
|
_Else -> {error, <<"bad-protocol">>}
|
|
|
|
end;
|
|
|
|
_Else -> {error, <<"bad-protocol">>}
|
|
|
|
end.
|
2011-08-16 00:25:03 +02:00
|
|
|
|
|
|
|
parse_attribute(Attribute) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
AttributeLen = byte_size(Attribute),
|
|
|
|
if AttributeLen >= 3 ->
|
|
|
|
AttributeS = binary_to_list(Attribute),
|
|
|
|
SecondChar = lists:nth(2, AttributeS),
|
|
|
|
case is_alpha(lists:nth(1, AttributeS)) of
|
|
|
|
true ->
|
|
|
|
if SecondChar == $= ->
|
|
|
|
String = str:substr(Attribute, 3),
|
|
|
|
{lists:nth(1, AttributeS), String};
|
|
|
|
true -> {error, <<"bad-format second char not equal sign">>}
|
|
|
|
end;
|
|
|
|
_Else -> {error, <<"bad-format first char not a letter">>}
|
|
|
|
end;
|
|
|
|
true -> {error, <<"bad-format attribute too short">>}
|
|
|
|
end.
|
2011-08-16 00:25:03 +02:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
unescape_username(<<"">>) -> <<"">>;
|
2011-08-16 00:25:03 +02:00
|
|
|
unescape_username(EscapedUsername) ->
|
2013-03-14 10:33:02 +01:00
|
|
|
Pos = str:str(EscapedUsername, <<"=">>),
|
|
|
|
if Pos == 0 -> EscapedUsername;
|
|
|
|
true ->
|
|
|
|
Start = str:substr(EscapedUsername, 1, Pos - 1),
|
|
|
|
End = str:substr(EscapedUsername, Pos),
|
|
|
|
EndLen = byte_size(End),
|
|
|
|
if EndLen < 3 -> error;
|
|
|
|
true ->
|
|
|
|
case str:substr(End, 1, 3) of
|
|
|
|
<<"=2C">> ->
|
|
|
|
<<Start/binary, ",",
|
|
|
|
(unescape_username(str:substr(End, 4)))/binary>>;
|
|
|
|
<<"=3D">> ->
|
|
|
|
<<Start/binary, "=",
|
|
|
|
(unescape_username(str:substr(End, 4)))/binary>>;
|
|
|
|
_Else -> error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end.
|
2011-08-16 00:25:03 +02:00
|
|
|
|
2013-03-14 10:33:02 +01:00
|
|
|
is_alpha(Char) when Char >= $a, Char =< $z -> true;
|
|
|
|
is_alpha(Char) when Char >= $A, Char =< $Z -> true;
|
|
|
|
is_alpha(_) -> false.
|