mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-22 16:20:52 +01:00
New SASL authentication method: SCRAM-SHA-1 (thanks to Stephen Röttger)(EJAB-1196)
This commit is contained in:
parent
1b7cc33a7f
commit
24f5c964cd
@ -1225,12 +1225,31 @@ When the storage is configured for ODBC, the ODBC server is
|
||||
configured with the \term{odbc\_server} option, see
|
||||
\ref{mysql} for MySQL, \ref{pgsql} for PostgreSQL, \ref{mssql} for MSSQL, and \ref{odbc} for generic ODBC.
|
||||
|
||||
The option \term{\{auth\_password\_format, plain|scram\}}
|
||||
defines in what format the users passwords are stored:
|
||||
\begin{description}
|
||||
\titem{plain}
|
||||
The password is stored as plain text in the database.
|
||||
This is risky because the passwords can be read if your database gets compromised.
|
||||
This is the default value.
|
||||
This format allows clients to authenticate using:
|
||||
the old Jabber Non-SASL (\xepref{0078}), \term{SASL PLAIN},
|
||||
\term{SASL DIGEST-MD5}, and \term{SASL SCRAM-SHA-1}.
|
||||
|
||||
\titem{scram}
|
||||
The password is not stored, only some information that allows to verify the hash provided by the client.
|
||||
It is impossible to obtain the original plain password from the stored information;
|
||||
for this reason, when this value is configured it cannot be changed to \term{plain} anymore.
|
||||
This format allows clients to authenticate using: \term{SASL PLAIN} and \term{SASL SCRAM-SHA-1}.
|
||||
\end{description}
|
||||
|
||||
Examples:
|
||||
\begin{itemize}
|
||||
\item To use internal Mnesia storage on all virtual hosts:
|
||||
\item To use internal Mnesia storage with hashed passwords on all virtual hosts:
|
||||
\begin{verbatim}
|
||||
{auth_method, storage}.
|
||||
{auth_storage, mnesia}.
|
||||
{auth_password_format, scram}.
|
||||
\end{verbatim}
|
||||
\item To use ODBC storage on all virtual hosts:
|
||||
\begin{verbatim}
|
||||
|
@ -43,7 +43,7 @@
|
||||
%% Require_Plain = bool().
|
||||
%% Registry entry of a supported SASL mechanism.
|
||||
|
||||
-record(sasl_mechanism, {mechanism, module, require_plain_password}).
|
||||
-record(sasl_mechanism, {mechanism, module, password_type}).
|
||||
|
||||
%% @type saslstate() = {sasl_state, Service, Myname, Realm, GetPassword, CheckPassword, CheckPasswordDigest, Mech_Mod, Mech_State}
|
||||
%% Service = string()
|
||||
@ -76,6 +76,7 @@ start() ->
|
||||
{keypos, #sasl_mechanism.mechanism}]),
|
||||
cyrsasl_plain:start([]),
|
||||
cyrsasl_digest:start([]),
|
||||
cyrsasl_scram:start([]),
|
||||
cyrsasl_anonymous:start([]),
|
||||
maybe_try_start_gssapi(),
|
||||
ok.
|
||||
@ -101,11 +102,11 @@ try_start_gssapi() ->
|
||||
%% Module = atom()
|
||||
%% Require_Plain = bool()
|
||||
|
||||
register_mechanism(Mechanism, Module, RequirePlainPassword) ->
|
||||
register_mechanism(Mechanism, Module, PasswordType) ->
|
||||
ets:insert(sasl_mechanism,
|
||||
#sasl_mechanism{mechanism = Mechanism,
|
||||
module = Module,
|
||||
require_plain_password = RequirePlainPassword}).
|
||||
password_type = PasswordType}).
|
||||
|
||||
% TODO use callbacks
|
||||
%-include("ejabberd.hrl").
|
||||
@ -153,17 +154,20 @@ check_credentials(_State, Props) ->
|
||||
%% Mechanism = string()
|
||||
|
||||
listmech(Host) ->
|
||||
RequirePlainPassword = ejabberd_auth:plain_password_required(Host),
|
||||
|
||||
Mechs = ets:select(sasl_mechanism,
|
||||
[{#sasl_mechanism{mechanism = '$1',
|
||||
require_plain_password = '$2',
|
||||
password_type = '$2',
|
||||
_ = '_'},
|
||||
if
|
||||
RequirePlainPassword ->
|
||||
[{'==', '$2', false}];
|
||||
true ->
|
||||
[]
|
||||
case catch ejabberd_auth:store_type(Host) of
|
||||
external ->
|
||||
[{'==', '$2', plain}];
|
||||
scram ->
|
||||
[{'/=', '$2', digest}];
|
||||
{'EXIT',{undef,[{Module,store_type,[]} | _]}} ->
|
||||
?WARNING_MSG("~p doesn't implement the function store_type/0", [Module]),
|
||||
[];
|
||||
_Else ->
|
||||
[]
|
||||
end,
|
||||
['$1']}]),
|
||||
filter_anonymous(Host, Mechs).
|
||||
@ -252,6 +256,13 @@ server_step(State, ClientIn) ->
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
{ok, Props, ServerOut} ->
|
||||
case check_credentials(State, Props) of
|
||||
ok ->
|
||||
{ok, Props, ServerOut};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
{continue, ServerOut, NewMechState} ->
|
||||
{continue, ServerOut,
|
||||
State#sasl_state{mech_state = NewMechState}};
|
||||
|
@ -42,7 +42,7 @@
|
||||
%% Opts = term()
|
||||
|
||||
start(_Opts) ->
|
||||
cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, false),
|
||||
cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, plain),
|
||||
ok.
|
||||
|
||||
%% @spec () -> ok
|
||||
|
@ -53,7 +53,7 @@
|
||||
%% Opts = term()
|
||||
|
||||
start(_Opts) ->
|
||||
cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, true).
|
||||
cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, digest).
|
||||
|
||||
%% @spec () -> ok
|
||||
|
||||
|
@ -42,7 +42,7 @@
|
||||
%% Opts = term()
|
||||
|
||||
start(_Opts) ->
|
||||
cyrsasl:register_mechanism("PLAIN", ?MODULE, false),
|
||||
cyrsasl:register_mechanism("PLAIN", ?MODULE, plain),
|
||||
ok.
|
||||
|
||||
%% @spec () -> ok
|
||||
|
197
src/cyrsasl_scram.erl
Normal file
197
src/cyrsasl_scram.erl
Normal file
@ -0,0 +1,197 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% 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>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne
|
||||
%%%
|
||||
%%% 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.
|
||||
%%%
|
||||
%%% 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., 59 Temple Place, Suite 330, Boston, MA
|
||||
%%% 02111-1307 USA
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
-module(cyrsasl_scram).
|
||||
-author('stephen.roettger@googlemail.com').
|
||||
|
||||
-export([start/1,
|
||||
stop/0,
|
||||
mech_new/1,
|
||||
mech_step/2]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
-include("cyrsasl.hrl").
|
||||
|
||||
-behaviour(cyrsasl).
|
||||
|
||||
-record(state, {step, stored_key, server_key, username, get_password, check_password,
|
||||
auth_message, client_nonce, server_nonce}).
|
||||
|
||||
-define(SALT_LENGTH, 16).
|
||||
-define(NONCE_LENGTH, 16).
|
||||
|
||||
start(_Opts) ->
|
||||
cyrsasl:register_mechanism("SCRAM-SHA-1", ?MODULE, scram).
|
||||
|
||||
stop() ->
|
||||
ok.
|
||||
|
||||
mech_new(#sasl_params{get_password=GetPassword}) ->
|
||||
{ok, #state{step = 2, get_password = GetPassword}}.
|
||||
|
||||
mech_step(#state{step = 2} = State, ClientIn) ->
|
||||
case string:tokens(ClientIn, ",") of
|
||||
[CBind, UserNameAttribute, ClientNonceAttribute] 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} ->
|
||||
case (State#state.get_password)(UserName) of
|
||||
{false, _} ->
|
||||
{error, 'not-authorized', "", UserName};
|
||||
{Ret, _AuthModule} ->
|
||||
{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 = string:substr(ClientIn, string:str(ClientIn, "n=")),
|
||||
ServerNonce = base64:encode_to_string(crypto:rand_bytes(?NONCE_LENGTH)),
|
||||
ServerFirstMessage = "r=" ++ ClientNonce ++ ServerNonce ++ "," ++
|
||||
"s=" ++ base64:encode_to_string(Salt) ++ "," ++
|
||||
"i=" ++ integer_to_list(IterationCount),
|
||||
{continue,
|
||||
ServerFirstMessage,
|
||||
State#state{step = 4, stored_key = StoredKey, server_key = ServerKey,
|
||||
auth_message = ClientFirstMessageBare ++ "," ++ ServerFirstMessage,
|
||||
client_nonce = ClientNonce, server_nonce = ServerNonce, username = UserName}}
|
||||
end;
|
||||
_Else ->
|
||||
{error, 'not-supported'}
|
||||
end
|
||||
end;
|
||||
_Else ->
|
||||
{error, 'bad-protocol'}
|
||||
end;
|
||||
_Else ->
|
||||
{error, 'bad-protocol'}
|
||||
end;
|
||||
mech_step(#state{step = 4} = State, ClientIn) ->
|
||||
case string:tokens(ClientIn, ",") of
|
||||
[GS2ChannelBindingAttribute, NonceAttribute, ClientProofAttribute] ->
|
||||
case parse_attribute(GS2ChannelBindingAttribute) of
|
||||
{$c, CVal} when (CVal == "biws") or (CVal == "eSws") ->
|
||||
%% biws is base64 for n,, => channelbinding not supported
|
||||
%% eSws is base64 for y,, => channelbinding supported by client only
|
||||
Nonce = State#state.client_nonce ++ State#state.server_nonce,
|
||||
case parse_attribute(NonceAttribute) of
|
||||
{$r, CompareNonce} when CompareNonce == Nonce ->
|
||||
case parse_attribute(ClientProofAttribute) of
|
||||
{$p, ClientProofB64} ->
|
||||
ClientProof = base64:decode(ClientProofB64),
|
||||
AuthMessage = State#state.auth_message ++ "," ++ string:substr(ClientIn, 1, string: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=" ++ base64:encode_to_string(ServerSignature)};
|
||||
true ->
|
||||
{error, 'bad-auth'}
|
||||
end;
|
||||
_Else ->
|
||||
{error, 'bad-protocol'}
|
||||
end;
|
||||
{$r, _} ->
|
||||
{error, 'bad-nonce'};
|
||||
_Else ->
|
||||
{error, 'bad-protocol'}
|
||||
end;
|
||||
_Else ->
|
||||
{error, 'bad-protocol'}
|
||||
end;
|
||||
_Else ->
|
||||
{error, 'bad-protocol'}
|
||||
end.
|
||||
|
||||
parse_attribute(Attribute) ->
|
||||
AttributeLen = string:len(Attribute),
|
||||
if
|
||||
AttributeLen > 3 ->
|
||||
SecondChar = lists:nth(2, Attribute),
|
||||
case is_alpha(lists:nth(1, Attribute)) of
|
||||
true ->
|
||||
if
|
||||
SecondChar == $= ->
|
||||
case string:substr(Attribute, 3) of
|
||||
String when is_list(String) ->
|
||||
{lists:nth(1, Attribute), String};
|
||||
_Else ->
|
||||
{error, 'bad-format failed'}
|
||||
end;
|
||||
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.
|
||||
|
||||
unescape_username("") ->
|
||||
"";
|
||||
unescape_username(EscapedUsername) ->
|
||||
Pos = string:str(EscapedUsername, "="),
|
||||
if
|
||||
Pos == 0 ->
|
||||
EscapedUsername;
|
||||
true ->
|
||||
Start = string:substr(EscapedUsername, 1, Pos-1),
|
||||
End = string:substr(EscapedUsername, Pos),
|
||||
EndLen = string:len(End),
|
||||
if
|
||||
EndLen < 3 ->
|
||||
error;
|
||||
true ->
|
||||
case string:substr(End, 1, 3) of
|
||||
"=2C" ->
|
||||
Start ++ "," ++ unescape_username(string:substr(End, 4));
|
||||
"=3D" ->
|
||||
Start ++ "=" ++ unescape_username(string:substr(End, 4));
|
||||
_Else ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
is_alpha(Char) when Char >= $a, Char =< $z ->
|
||||
true;
|
||||
is_alpha(Char) when Char >= $A, Char =< $Z ->
|
||||
true;
|
||||
is_alpha(_) ->
|
||||
true.
|
||||
|
@ -8,6 +8,7 @@
|
||||
cyrsasl,
|
||||
cyrsasl_digest,
|
||||
cyrsasl_plain,
|
||||
cyrsasl_scram,
|
||||
ejabberd_admin,
|
||||
ejabberd_app,
|
||||
ejabberd_auth_anonymous,
|
||||
|
@ -295,6 +295,15 @@
|
||||
%%
|
||||
%%{host_config, "public.example.org", [{auth_method, [internal, anonymous]}]}.
|
||||
|
||||
%%
|
||||
%% auth_password_format: Format of storing users passwords
|
||||
%% The default format is plain text.
|
||||
%% If you change to hashed scram, you can never go back to plain.
|
||||
%% This option is only supported by the 'storage' auth_method.
|
||||
%%
|
||||
{auth_password_format, plain}.
|
||||
%%{auth_password_format, scram}.
|
||||
|
||||
|
||||
%%%. ==============
|
||||
%%%' DATABASE SETUP
|
||||
|
@ -47,6 +47,9 @@
|
||||
|
||||
%%-define(DBGFSM, true).
|
||||
|
||||
-record(scram, {storedkey, serverkey, salt, iterationcount}).
|
||||
-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096).
|
||||
|
||||
%% ---------------------------------
|
||||
%% Logging mechanism
|
||||
|
||||
|
@ -50,6 +50,7 @@
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
plain_password_required/1,
|
||||
store_type/1,
|
||||
entropy/1
|
||||
]).
|
||||
|
||||
@ -105,12 +106,31 @@ stop_methods(Host, Method) when is_atom(Method) ->
|
||||
%% @spec (Server) -> bool()
|
||||
%% Server = string()
|
||||
|
||||
%% This is only executed by ejabberd_c2s for non-SASL auth client
|
||||
plain_password_required(Server) when is_list(Server) ->
|
||||
lists:any(
|
||||
fun(M) ->
|
||||
M:plain_password_required()
|
||||
end, auth_modules(Server)).
|
||||
|
||||
%% @spec (Server) -> bool()
|
||||
%% Server = string()
|
||||
|
||||
store_type(Server) ->
|
||||
lists:foldl(
|
||||
fun(_, external) ->
|
||||
external;
|
||||
(M, scram) ->
|
||||
case M:store_type() of
|
||||
external ->
|
||||
external;
|
||||
_Else ->
|
||||
scram
|
||||
end;
|
||||
(M, plain) ->
|
||||
M:store_type()
|
||||
end, plain, auth_modules(Server)).
|
||||
|
||||
%% @spec (User, Server, Password) -> bool()
|
||||
%% User = string()
|
||||
%% Server = string()
|
||||
@ -342,8 +362,10 @@ get_password_s(User, Server) when is_list(User), is_list(Server) ->
|
||||
case get_password(User, Server) of
|
||||
false ->
|
||||
"";
|
||||
Password ->
|
||||
Password
|
||||
Password when is_list(Password) ->
|
||||
Password;
|
||||
_ ->
|
||||
""
|
||||
end.
|
||||
|
||||
%% @spec (User, Server) -> {Password, AuthModule} | {false, none}
|
||||
|
@ -52,6 +52,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
store_type/0,
|
||||
plain_password_required/0]).
|
||||
|
||||
-include_lib("exmpp/include/exmpp.hrl").
|
||||
@ -360,6 +361,9 @@ remove_user(_User, _Server, _Password) ->
|
||||
plain_password_required() ->
|
||||
false.
|
||||
|
||||
store_type() ->
|
||||
plain.
|
||||
|
||||
update_tables() ->
|
||||
case catch mnesia:table_info(anonymous, local_content) of
|
||||
false ->
|
||||
|
@ -44,6 +44,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -99,6 +100,9 @@ plain_password_required() ->
|
||||
%% Server = string()
|
||||
%% Password = string()
|
||||
|
||||
store_type() ->
|
||||
external.
|
||||
|
||||
check_password(User, Server, Password) ->
|
||||
case get_cache_option(Server) of
|
||||
false -> check_password_extauth(User, Server, Password);
|
||||
|
@ -54,6 +54,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -184,6 +185,9 @@ plain_password_required() ->
|
||||
%% Server = string()
|
||||
%% Password = string()
|
||||
|
||||
store_type() ->
|
||||
external.
|
||||
|
||||
check_password(User, Server, Password) ->
|
||||
%% In LDAP spec: empty password means anonymous authentication.
|
||||
%% As ejabberd is providing other anonymous authentication mechanisms
|
||||
|
@ -40,6 +40,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -171,6 +172,9 @@ remove_user(_User, _Server, _Password) ->
|
||||
plain_password_required() ->
|
||||
true.
|
||||
|
||||
store_type() ->
|
||||
external.
|
||||
|
||||
%%====================================================================
|
||||
%% Internal functions
|
||||
%%====================================================================
|
||||
|
@ -44,10 +44,27 @@
|
||||
%%% user_host = {Username::string(), Host::string()}
|
||||
%%% password = string()
|
||||
%%%
|
||||
%%% 3.0.0-beta / mnesia / passwd
|
||||
%%% user_host = {Username::string(), Host::string()}
|
||||
%%% password = string()
|
||||
%%% storedkey = base64 binary()
|
||||
%%% serverkey = base64 binary()
|
||||
%%% iterationcount = integer()
|
||||
%%% salt = base64 binary()
|
||||
%%%
|
||||
%%% 3.0.0-alpha / odbc / passwd
|
||||
%%% user = varchar150
|
||||
%%% host = varchar150
|
||||
%%% password = text
|
||||
%%%
|
||||
%%% 3.0.0-beta / odbc / passwd
|
||||
%%% user = varchar150
|
||||
%%% host = varchar150
|
||||
%%% password = base64 text
|
||||
%%% storedkey = base64 text
|
||||
%%% serverkey = base64 text
|
||||
%%% iterationcount = integer
|
||||
%%% salt = base64 text
|
||||
|
||||
-module(ejabberd_auth_storage).
|
||||
-author('alexey@process-one.net').
|
||||
@ -69,14 +86,17 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
|
||||
-record(passwd, {user_host, password}).
|
||||
-record(passwd, {user_host, password, storedkey, serverkey, salt, iterationcount}).
|
||||
-record(reg_users_counter, {vhost, count}).
|
||||
|
||||
-define(SALT_LENGTH, 16).
|
||||
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% API
|
||||
%%%----------------------------------------------------------------------
|
||||
@ -95,13 +115,19 @@ start(Host) ->
|
||||
[{odbc_host, Host},
|
||||
{disc_copies, [node()]},
|
||||
{attributes, record_info(fields, passwd)},
|
||||
{types, [{user_host, {text, text}}]}
|
||||
{types, [{user_host, {text, text}},
|
||||
{storedkey, binary},
|
||||
{serverkey, binary},
|
||||
{salt, binary},
|
||||
{iterationcount, int}]}
|
||||
]),
|
||||
update_table(Host, Backend),
|
||||
maybe_scram_passwords(Host),
|
||||
mnesia:create_table(reg_users_counter,
|
||||
[{ram_copies, [node()]},
|
||||
{attributes, record_info(fields, reg_users_counter)}]),
|
||||
update_reg_users_counter_table(Host),
|
||||
maybe_alert_password_scrammed_without_option(Host),
|
||||
ok.
|
||||
|
||||
stop(_Host) ->
|
||||
@ -120,7 +146,16 @@ update_reg_users_counter_table(Server) ->
|
||||
%% @spec () -> bool()
|
||||
|
||||
plain_password_required() ->
|
||||
false.
|
||||
case is_scrammed(?MYNAME) of
|
||||
false -> false;
|
||||
true -> true
|
||||
end.
|
||||
|
||||
store_type() ->
|
||||
case is_scrammed(?MYNAME) of
|
||||
false -> plain; %% allows: PLAIN DIGEST-MD5 SCRAM
|
||||
true -> scram %% allows: PLAIN SCRAM
|
||||
end.
|
||||
|
||||
%% @spec (User, Server, Password) -> bool()
|
||||
%% User = string()
|
||||
@ -132,6 +167,8 @@ check_password(User, Server, Password) ->
|
||||
LServer = exmpp_stringprep:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch gen_storage:dirty_read(LServer, {passwd, US}) of
|
||||
[#passwd{password = ""} = Passwd] ->
|
||||
is_password_scram_valid(Password, Passwd);
|
||||
[#passwd{password = Password}] ->
|
||||
Password /= "";
|
||||
_ ->
|
||||
@ -150,6 +187,19 @@ check_password(User, Server, Password, Digest, DigestGen) ->
|
||||
LServer = exmpp_stringprep:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch gen_storage:dirty_read(LServer, {passwd, US}) of
|
||||
[#passwd{password = ""} = Passwd] ->
|
||||
Passwd = base64:decode(Passwd#passwd.storedkey),
|
||||
DigRes = if
|
||||
Digest /= "" ->
|
||||
Digest == DigestGen(Passwd);
|
||||
true ->
|
||||
false
|
||||
end,
|
||||
if DigRes ->
|
||||
true;
|
||||
true ->
|
||||
(Passwd == Password) and (Password /= "")
|
||||
end;
|
||||
[#passwd{password = Passwd}] ->
|
||||
DigRes = if
|
||||
Digest /= "" ->
|
||||
@ -182,9 +232,11 @@ set_password(User, Server, Password) ->
|
||||
US ->
|
||||
%% TODO: why is this a transaction?
|
||||
F = fun() ->
|
||||
gen_storage:write(LServer,
|
||||
#passwd{user_host = US,
|
||||
password = Password})
|
||||
Passwd = case is_scrammed(LServer) and (Password /= "") of
|
||||
true -> password_to_scram(Password, #passwd{user_host=US});
|
||||
false -> #passwd{user_host = US, password = Password}
|
||||
end,
|
||||
gen_storage:write(LServer, Passwd)
|
||||
end,
|
||||
{atomic, ok} = gen_storage:transaction(LServer, passwd, F),
|
||||
ok
|
||||
@ -207,9 +259,11 @@ try_register(User, Server, Password) ->
|
||||
F = fun() ->
|
||||
case gen_storage:read(LServer, {passwd, US}) of
|
||||
[] ->
|
||||
gen_storage:write(LServer,
|
||||
#passwd{user_host = US,
|
||||
password = Password}),
|
||||
Passwd = case is_scrammed(LServer) and (Password /= "") of
|
||||
true -> password_to_scram(Password, #passwd{user_host=US});
|
||||
false -> #passwd{user_host = US, password = Password}
|
||||
end,
|
||||
gen_storage:write(LServer, Passwd),
|
||||
mnesia:dirty_update_counter(
|
||||
reg_users_counter,
|
||||
exmpp_jid:prep_domain(exmpp_jid:parse(Server)), 1),
|
||||
@ -352,9 +406,14 @@ get_password(User, Server) ->
|
||||
LServer = exmpp_stringprep:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch gen_storage:dirty_read(LServer, passwd, US) of
|
||||
[#passwd{password = Password}] ->
|
||||
[#passwd{password = ""} = Passwd] ->
|
||||
{base64:decode(Passwd#passwd.storedkey),
|
||||
base64:decode(Passwd#passwd.serverkey),
|
||||
base64:decode(Passwd#passwd.salt),
|
||||
Passwd#passwd.iterationcount};
|
||||
[#passwd{password = Password}] ->
|
||||
Password;
|
||||
_ ->
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
catch
|
||||
@ -373,8 +432,8 @@ get_password_s(User, Server) ->
|
||||
LServer = exmpp_stringprep:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch gen_storage:dirty_read(LServer, passwd, US) of
|
||||
[#passwd{password = Password}] ->
|
||||
Password;
|
||||
[#passwd{password = Password}] ->
|
||||
Password;
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
@ -441,13 +500,21 @@ remove_user(User, Server, Password) ->
|
||||
US = {LUser, LServer},
|
||||
F = fun() ->
|
||||
case gen_storage:read(LServer, {passwd, US}) of
|
||||
[#passwd{password = ""} = Passwd] ->
|
||||
case is_password_scram_valid(Password, Passwd) of
|
||||
true ->
|
||||
gen_storage:delete(LServer, {passwd, US}),
|
||||
mnesia:dirty_update_counter(reg_users_counter,
|
||||
LServer, -1),
|
||||
ok;
|
||||
false ->
|
||||
not_allowed
|
||||
end;
|
||||
[#passwd{password = Password}] ->
|
||||
gen_storage:delete(LServer, {passwd, US}),
|
||||
mnesia:dirty_update_counter(reg_users_counter,
|
||||
exmpp_jid:prep_domain(exmpp_jid:parse(Server)), -1),
|
||||
ok;
|
||||
[_] ->
|
||||
not_allowed;
|
||||
_ ->
|
||||
not_exists
|
||||
end
|
||||
@ -463,13 +530,120 @@ remove_user(User, Server, Password) ->
|
||||
bad_request
|
||||
end.
|
||||
|
||||
%%%
|
||||
%%% SCRAM
|
||||
%%%
|
||||
|
||||
%% The passwords are stored scrammed in the table either if the option says so,
|
||||
%% or if at least the first password is empty.
|
||||
is_scrammed(Host) ->
|
||||
case action_password_format(Host) of
|
||||
scram -> true;
|
||||
must_scram -> true;
|
||||
plain -> false;
|
||||
forced_scram -> true
|
||||
end.
|
||||
|
||||
action_password_format(Host) ->
|
||||
OptionScram = is_option_scram(),
|
||||
case {OptionScram, get_format_first_element(Host)} of
|
||||
{true, scram} -> scram;
|
||||
{true, any} -> scram;
|
||||
{true, plain} -> must_scram;
|
||||
{false, plain} -> plain;
|
||||
{false, any} -> plain;
|
||||
{false, scram} -> forced_scram
|
||||
end.
|
||||
|
||||
get_format_first_element(Host) ->
|
||||
case gen_storage:dirty_select(Host, passwd, []) of
|
||||
[] -> any;
|
||||
[#passwd{password = ""} | _] -> scram;
|
||||
[#passwd{} | _] -> plain
|
||||
end.
|
||||
|
||||
is_option_scram() ->
|
||||
scram == ejabberd_config:get_local_option({auth_password_format, ?MYNAME}).
|
||||
|
||||
maybe_alert_password_scrammed_without_option(Host) ->
|
||||
case is_scrammed(Host) andalso not is_option_scram() of
|
||||
true ->
|
||||
?ERROR_MSG("Some passwords were stored in the database as SCRAM, "
|
||||
"but 'auth_password_format' is not configured 'scram'. "
|
||||
"The option will now be considered to be 'scram'.", []);
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
|
||||
maybe_scram_passwords(Host) ->
|
||||
case action_password_format(Host) of
|
||||
must_scram -> scram_passwords(Host);
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
scram_passwords(Host) ->
|
||||
Backend =
|
||||
case ejabberd_config:get_local_option({auth_storage, Host}) of
|
||||
undefined -> mnesia;
|
||||
B -> B
|
||||
end,
|
||||
scram_passwords(Host, Backend).
|
||||
scram_passwords(Host, mnesia) ->
|
||||
?INFO_MSG("Converting the passwords stored in odbc for host ~p into SCRAM bits", [Host]),
|
||||
gen_storage_migration:migrate_mnesia(
|
||||
Host, passwd,
|
||||
[{passwd, [user_host, password, storedkey, serverkey, iterationcount, salt],
|
||||
fun(#passwd{password = Password} = Passwd) ->
|
||||
password_to_scram(Password, Passwd)
|
||||
end}]);
|
||||
scram_passwords(Host, odbc) ->
|
||||
?INFO_MSG("Converting the passwords stored in odbc for host ~p into SCRAM bits", [Host]),
|
||||
gen_storage_migration:migrate_odbc(
|
||||
Host, [passwd],
|
||||
[{"passwd", ["user", "host", "password", "storedkey", "serverkey", "iterationcount", "salt"],
|
||||
fun(_, User, Host2, Password, _Storedkey, _Serverkey, _Iterationcount, _Salt) ->
|
||||
password_to_scram(Password, #passwd{user_host = {User, Host2}})
|
||||
end}]).
|
||||
|
||||
password_to_scram(Password, Passwd) ->
|
||||
password_to_scram(Password, Passwd, ?SCRAM_DEFAULT_ITERATION_COUNT).
|
||||
|
||||
password_to_scram(Password, Passwd, IterationCount) ->
|
||||
Salt = crypto:rand_bytes(?SALT_LENGTH),
|
||||
SaltedPassword = scram:salted_password(Password, Salt, IterationCount),
|
||||
StoredKey = scram:stored_key(scram:client_key(SaltedPassword)),
|
||||
ServerKey = scram:server_key(SaltedPassword),
|
||||
Passwd#passwd{password = "",
|
||||
storedkey = base64:encode(StoredKey),
|
||||
salt = base64:encode(Salt),
|
||||
iterationcount = IterationCount,
|
||||
serverkey = base64:encode(ServerKey)}.
|
||||
|
||||
is_password_scram_valid(Password, Passwd) ->
|
||||
IterationCount = Passwd#passwd.iterationcount,
|
||||
Salt = base64:decode(Passwd#passwd.salt),
|
||||
SaltedPassword = scram:salted_password(Password, Salt, IterationCount),
|
||||
StoredKey = scram:stored_key(scram:client_key(SaltedPassword)),
|
||||
(base64:decode(Passwd#passwd.storedkey) == StoredKey).
|
||||
|
||||
|
||||
update_table(Host, mnesia) ->
|
||||
gen_storage_migration:migrate_mnesia(
|
||||
Host, passwd,
|
||||
[{passwd, [us, password],
|
||||
fun({passwd, {User, _Host}, Password}) ->
|
||||
#passwd{user_host = {User, Host},
|
||||
password = Password}
|
||||
case is_list(Password) of
|
||||
true ->
|
||||
#passwd{user_host = {User, Host},
|
||||
password = Password};
|
||||
false ->
|
||||
#passwd{user_host = {User, Host},
|
||||
password = "",
|
||||
storedkey = Password#scram.storedkey,
|
||||
serverkey = Password#scram.serverkey,
|
||||
salt = Password#scram.salt,
|
||||
iterationcount = Password#scram.iterationcount}
|
||||
end
|
||||
end}]);
|
||||
update_table(Host, odbc) ->
|
||||
gen_storage_migration:migrate_odbc(
|
||||
|
@ -844,6 +844,21 @@ wait_for_sasl_response({xmlstreamelement, #xmlel{ns = NS, name = Name} = El},
|
||||
StateData#state.socket),
|
||||
send_element(StateData, exmpp_server_sasl:success()),
|
||||
U = proplists:get_value(username, Props),
|
||||
AuthModule = proplists:get_value(auth_module, Props),
|
||||
?INFO_MSG("(~w) Accepted authentication for ~s by ~s",
|
||||
[StateData#state.socket, U, AuthModule]),
|
||||
fsm_next_state(wait_for_stream,
|
||||
StateData#state{
|
||||
streamid = new_id(),
|
||||
authenticated = true,
|
||||
auth_module = AuthModule,
|
||||
user = list_to_binary(U)});
|
||||
{ok, Props, ServerOut} ->
|
||||
catch (StateData#state.sockmod):reset_stream(
|
||||
StateData#state.socket),
|
||||
send_element(StateData, exmpp_server_sasl:success(ServerOut)),
|
||||
U = proplists:get_value(username, Props),
|
||||
|
||||
AuthModule = proplists:get_value(auth_module, Props),
|
||||
?INFO_MSG("(~w) Accepted authentication for ~s by ~s",
|
||||
[StateData#state.socket, U, AuthModule]),
|
||||
|
@ -159,21 +159,24 @@ process_element(El,State) ->
|
||||
|
||||
add_user(El, Domain) ->
|
||||
User = exmpp_xml:get_attribute(El,<<"name">>,none),
|
||||
PasswordFormat = exmpp_xml:get_attribute(El,<<"password-format">>,none),
|
||||
Password = exmpp_xml:get_attribute(El,<<"password">>,none),
|
||||
add_user(El, Domain, User, Password).
|
||||
add_user(El, Domain, User, PasswordFormat, Password).
|
||||
|
||||
%% @spec (El::xmlel(), Domain::string(), User::binary(), Password::binary() | none)
|
||||
%% @spec (El::xmlel(), Domain::string(), User::binary(), PasswordFormat, Password::binary() | none)
|
||||
%% -> ok | {error, ErrorText::string()}
|
||||
%% PasswordFormat = <<"plaintext">> | <<"scram">>
|
||||
%% @doc Add a new user to the database.
|
||||
%% If user already exists, it will be only updated.
|
||||
add_user(El, Domain, User, none) ->
|
||||
add_user(El, Domain, User, <<"plaintext">>, none) ->
|
||||
io:format("Account ~s@~s will not be created, updating it...~n",
|
||||
[User, Domain]),
|
||||
io:format(""),
|
||||
populate_user_with_elements(El, Domain, User),
|
||||
ok;
|
||||
add_user(El, Domain, User, Password) ->
|
||||
case create_user(User,Password,Domain) of
|
||||
add_user(El, Domain, User, PasswordFormat, Password) ->
|
||||
Password2 = prepare_password(PasswordFormat, Password, El),
|
||||
case create_user(User,Password2,Domain) of
|
||||
ok ->
|
||||
populate_user_with_elements(El, Domain, User),
|
||||
ok;
|
||||
@ -188,6 +191,21 @@ add_user(El, Domain, User, Password) ->
|
||||
{error, Other}
|
||||
end.
|
||||
|
||||
prepare_password(<<"plaintext">>, PasswordBinary, _El) ->
|
||||
?BTL(PasswordBinary);
|
||||
prepare_password(<<"scram">>, none, El) ->
|
||||
ScramEl = exmpp_xml:get_element(El, 'scram-hash'),
|
||||
#scram{storedkey = base64:decode(exmpp_xml:get_attribute(
|
||||
ScramEl, <<"stored-key">>, none)),
|
||||
serverkey = base64:decode(exmpp_xml:get_attribute(
|
||||
ScramEl, <<"server-key">>, none)),
|
||||
salt = base64:decode(exmpp_xml:get_attribute(
|
||||
ScramEl, <<"salt">>, none)),
|
||||
iterationcount = list_to_integer(exmpp_xml:get_attribute_as_list(
|
||||
ScramEl, <<"iteration-count">>,
|
||||
?SCRAM_DEFAULT_ITERATION_COUNT))
|
||||
}.
|
||||
|
||||
populate_user_with_elements(El, Domain, User) ->
|
||||
exmpp_xml:foreach(
|
||||
fun (_,Child) ->
|
||||
@ -482,10 +500,23 @@ export_user(Fd, Username, Host) ->
|
||||
|
||||
%% @spec (Username::string(), Host::string()) -> string()
|
||||
extract_user(Username, Host) ->
|
||||
Password = ejabberd_auth:get_password_s(Username, Host),
|
||||
Password = ejabberd_auth:get_password(Username, Host),
|
||||
PasswordStr = build_password_string(Password),
|
||||
UserInfo = [extract_user_info(InfoName, Username, Host) || InfoName <- [roster, offline, private, vcard]],
|
||||
UserInfoString = lists:flatten(UserInfo),
|
||||
io_lib:format("<user name='~s' password='~s'>~s</user>", [Username, Password, UserInfoString]).
|
||||
io_lib:format("<user name='~s' ~s ~s</user>",
|
||||
[Username, PasswordStr, UserInfoString]).
|
||||
|
||||
build_password_string({StoredKey, ServerKey, Salt, IterationCount}) ->
|
||||
io_lib:format("password-format='scram'>"
|
||||
"<scram-hash stored-key='~s' server-key='~s' "
|
||||
"salt='~s' iteration-count='~w'/> ",
|
||||
[base64:encode_to_string(StoredKey),
|
||||
base64:encode_to_string(ServerKey),
|
||||
base64:encode_to_string(Salt),
|
||||
IterationCount]);
|
||||
build_password_string(Password) when is_list(Password) ->
|
||||
io_lib:format("password-format='plaintext' password='~s'>", [Password]).
|
||||
|
||||
%% @spec (InfoName::atom(), Username::string(), Host::string()) -> string()
|
||||
extract_user_info(roster, Username, Host) ->
|
||||
|
81
src/scram.erl
Normal file
81
src/scram.erl
Normal file
@ -0,0 +1,81 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : scram.erl
|
||||
%%% Author : Stephen Röttger <stephen.roettger@googlemail.com>
|
||||
%%% Purpose : SCRAM (RFC 5802)
|
||||
%%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne
|
||||
%%%
|
||||
%%% 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.
|
||||
%%%
|
||||
%%% 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., 59 Temple Place, Suite 330, Boston, MA
|
||||
%%% 02111-1307 USA
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
-module(scram).
|
||||
-author('stephen.roettger@googlemail.com').
|
||||
|
||||
%% External exports
|
||||
-export([salted_password/3,
|
||||
stored_key/1,
|
||||
server_key/1,
|
||||
server_signature/2,
|
||||
client_signature/2,
|
||||
client_key/1,
|
||||
client_key/2
|
||||
]).
|
||||
|
||||
salted_password(Password, Salt, IterationCount) ->
|
||||
hi(jlib:nameprep(Password), Salt, IterationCount).
|
||||
|
||||
client_key(SaltedPassword) ->
|
||||
crypto:sha_mac(SaltedPassword, "Client Key").
|
||||
|
||||
stored_key(ClientKey) ->
|
||||
crypto:sha(ClientKey).
|
||||
|
||||
server_key(SaltedPassword) ->
|
||||
crypto:sha_mac(SaltedPassword, "Server Key").
|
||||
|
||||
client_signature(StoredKey, AuthMessage) ->
|
||||
crypto:sha_mac(StoredKey, AuthMessage).
|
||||
|
||||
client_key(ClientProof, ClientSignature) ->
|
||||
binary:list_to_bin(lists:zipwith(fun(X, Y) ->
|
||||
X bxor Y
|
||||
end,
|
||||
binary:bin_to_list(ClientProof),
|
||||
binary:bin_to_list(ClientSignature))).
|
||||
|
||||
server_signature(ServerKey, AuthMessage) ->
|
||||
crypto:sha_mac(ServerKey, AuthMessage).
|
||||
|
||||
hi(Password, Salt, IterationCount) ->
|
||||
U1 = crypto:sha_mac(Password, string:concat(binary:bin_to_list(Salt), [0,0,0,1])),
|
||||
binary:list_to_bin(lists:zipwith(fun(X, Y) ->
|
||||
X bxor Y
|
||||
end,
|
||||
binary:bin_to_list(U1),
|
||||
binary:bin_to_list(hi_round(Password, U1, IterationCount-1)))).
|
||||
|
||||
hi_round(Password, UPrev, 1) ->
|
||||
crypto:sha_mac(Password, UPrev);
|
||||
hi_round(Password, UPrev, IterationCount) ->
|
||||
U = crypto:sha_mac(Password, UPrev),
|
||||
binary:list_to_bin(lists:zipwith(fun(X, Y) ->
|
||||
X bxor Y
|
||||
end,
|
||||
binary:bin_to_list(U),
|
||||
binary:bin_to_list(hi_round(Password, U, IterationCount-1)))).
|
Loading…
Reference in New Issue
Block a user