mirror of
https://github.com/processone/ejabberd.git
synced 2024-12-22 17:28:25 +01:00
Fix and improve support for SCRAM auth method (EJAB-1196)
This commit is contained in:
parent
24852b9be8
commit
1ee6eae684
@ -1221,6 +1221,24 @@ Account creation is only supported by internal, external and odbc methods.
|
||||
\ejabberd{} uses its internal Mnesia database as the default authentication method.
|
||||
The value \term{internal} will enable the internal authentication method.
|
||||
|
||||
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 authentication on \jid{example.org} and LDAP
|
||||
@ -1229,9 +1247,10 @@ Examples:
|
||||
{host_config, "example.org", [{auth_method, [internal]}]}.
|
||||
{host_config, "example.net", [{auth_method, [ldap]}]}.
|
||||
\end{verbatim}
|
||||
\item To use internal authentication on all virtual hosts:
|
||||
\item To use internal authentication with hashed passwords on all virtual hosts:
|
||||
\begin{verbatim}
|
||||
{auth_method, internal}.
|
||||
{auth_password_format, scram}.
|
||||
\end{verbatim}
|
||||
\end{itemize}
|
||||
|
||||
|
@ -34,6 +34,8 @@
|
||||
server_start/3,
|
||||
server_step/2]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
|
||||
-record(sasl_mechanism, {mechanism, module, password_type}).
|
||||
-record(sasl_state, {service, myname, realm,
|
||||
get_password, check_password, check_password_digest,
|
||||
@ -102,11 +104,14 @@ listmech(Host) ->
|
||||
[{#sasl_mechanism{mechanism = '$1',
|
||||
password_type = '$2',
|
||||
_ = '_'},
|
||||
case ejabberd_auth:storage_type(Host) of
|
||||
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,
|
||||
|
@ -39,7 +39,6 @@
|
||||
-record(state, {step, stored_key, server_key, username, get_password, check_password,
|
||||
auth_message, client_nonce, server_nonce}).
|
||||
|
||||
-define(DEFAULT_ITERATION_COUNT, 4096).
|
||||
-define(SALT_LENGTH, 16).
|
||||
-define(NONCE_LENGTH, 16).
|
||||
|
||||
@ -54,9 +53,9 @@ mech_new(_Host, GetPassword, _CheckPassword, _CheckPasswordDigest) ->
|
||||
|
||||
mech_step(#state{step = 2} = State, ClientIn) ->
|
||||
case string:tokens(ClientIn, ",") of
|
||||
["n", UserNameAttribute, ClientNonceAttribute] ->
|
||||
[CBind, UserNameAttribute, ClientNonceAttribute] when (CBind == "y") or (CBind == "n") ->
|
||||
case parse_attribute(UserNameAttribute) of
|
||||
{$n, EscapedUserName} ->
|
||||
{_, EscapedUserName} ->
|
||||
case unescape_username(EscapedUserName) of
|
||||
error ->
|
||||
{error, "protocol-error-bad-username"};
|
||||
@ -72,8 +71,9 @@ mech_step(#state{step = 2} = State, ClientIn) ->
|
||||
Ret;
|
||||
true ->
|
||||
TempSalt = crypto:rand_bytes(?SALT_LENGTH),
|
||||
SaltedPassword = scram:salted_password(Ret, TempSalt, ?DEFAULT_ITERATION_COUNT),
|
||||
{scram:stored_key(scram:client_key(SaltedPassword)), TempSalt, ?DEFAULT_ITERATION_COUNT}
|
||||
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)),
|
||||
@ -102,7 +102,9 @@ mech_step(#state{step = 4} = State, ClientIn) ->
|
||||
case string:tokens(ClientIn, ",") of
|
||||
[GS2ChannelBindingAttribute, NonceAttribute, ClientProofAttribute] ->
|
||||
case parse_attribute(GS2ChannelBindingAttribute) of
|
||||
{$c, "biws"} -> %biws is base64 for n,, => channelbinding not supported
|
||||
{$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 ->
|
||||
|
@ -218,6 +218,10 @@
|
||||
%% comment this line and enable the correct ones.
|
||||
%%
|
||||
{auth_method, internal}.
|
||||
%%
|
||||
%% Store the plain passwords or hashed for SCRAM:
|
||||
%%{auth_password_format, plain}.
|
||||
%%{auth_password_format, scram}.
|
||||
|
||||
%%
|
||||
%% Authentication using external script
|
||||
|
@ -37,6 +37,9 @@
|
||||
|
||||
%%-define(DBGFSM, true).
|
||||
|
||||
-record(scram, {storedkey, serverkey, salt, iterationcount}).
|
||||
-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096).
|
||||
|
||||
%% ---------------------------------
|
||||
%% Logging mechanism
|
||||
|
||||
|
@ -50,7 +50,7 @@
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
plain_password_required/1,
|
||||
storage_type/1,
|
||||
store_type/1,
|
||||
entropy/1
|
||||
]).
|
||||
|
||||
@ -70,25 +70,26 @@ start() ->
|
||||
end, auth_modules(Host))
|
||||
end, ?MYHOSTS).
|
||||
|
||||
%% This is only executed by ejabberd_c2s for non-SASL auth client
|
||||
plain_password_required(Server) ->
|
||||
lists:any(
|
||||
fun(M) ->
|
||||
M:plain_password_required()
|
||||
end, auth_modules(Server)).
|
||||
|
||||
storage_type(Server) ->
|
||||
store_type(Server) ->
|
||||
lists:foldl(
|
||||
fun(_, external) ->
|
||||
external;
|
||||
(M, scram) ->
|
||||
case M:storage_type() of
|
||||
case M:store_type() of
|
||||
external ->
|
||||
external;
|
||||
_Else ->
|
||||
scram
|
||||
end;
|
||||
(M, plain) ->
|
||||
M:storage_type()
|
||||
M:store_type()
|
||||
end, plain, auth_modules(Server)).
|
||||
|
||||
%% @doc Check if the user and password can login in server.
|
||||
@ -248,8 +249,10 @@ get_password_s(User, Server) ->
|
||||
case get_password(User, Server) of
|
||||
false ->
|
||||
"";
|
||||
Password ->
|
||||
Password
|
||||
Password when is_list(Password) ->
|
||||
Password;
|
||||
_ ->
|
||||
""
|
||||
end.
|
||||
|
||||
%% @doc Get the password of the user and the auth module.
|
||||
|
@ -51,7 +51,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
storage_type/0,
|
||||
store_type/0,
|
||||
plain_password_required/0]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
@ -249,5 +249,5 @@ remove_user(_User, _Server, _Password) ->
|
||||
plain_password_required() ->
|
||||
false.
|
||||
|
||||
storage_type() ->
|
||||
store_type() ->
|
||||
plain.
|
||||
|
@ -43,7 +43,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
storage_type/0,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -79,7 +79,7 @@ check_cache_last_options(Server) ->
|
||||
plain_password_required() ->
|
||||
true.
|
||||
|
||||
storage_type() ->
|
||||
store_type() ->
|
||||
external.
|
||||
|
||||
check_password(User, Server, Password) ->
|
||||
|
@ -43,7 +43,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
storage_type/0,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -52,6 +52,8 @@
|
||||
-record(passwd, {us, password}).
|
||||
-record(reg_users_counter, {vhost, count}).
|
||||
|
||||
-define(SALT_LENGTH, 16).
|
||||
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% API
|
||||
%%%----------------------------------------------------------------------
|
||||
@ -63,6 +65,7 @@ start(Host) ->
|
||||
{attributes, record_info(fields, reg_users_counter)}]),
|
||||
update_table(),
|
||||
update_reg_users_counter_table(Host),
|
||||
maybe_alert_password_scrammed_without_option(),
|
||||
ok.
|
||||
|
||||
update_reg_users_counter_table(Server) ->
|
||||
@ -76,18 +79,26 @@ update_reg_users_counter_table(Server) ->
|
||||
mnesia:sync_dirty(F).
|
||||
|
||||
plain_password_required() ->
|
||||
false.
|
||||
case is_scrammed() of
|
||||
false -> false;
|
||||
true -> true
|
||||
end.
|
||||
|
||||
storage_type() ->
|
||||
plain.
|
||||
store_type() ->
|
||||
case is_scrammed() of
|
||||
false -> plain; %% allows: PLAIN DIGEST-MD5 SCRAM
|
||||
true -> scram %% allows: PLAIN SCRAM
|
||||
end.
|
||||
|
||||
check_password(User, Server, Password) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read({passwd, US}) of
|
||||
[#passwd{password = Password}] ->
|
||||
[#passwd{password = Password}] when is_list(Password) ->
|
||||
Password /= "";
|
||||
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||
is_password_scram_valid(Password, Scram);
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
@ -97,7 +108,20 @@ check_password(User, Server, Password, Digest, DigestGen) ->
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read({passwd, US}) of
|
||||
[#passwd{password = Passwd}] ->
|
||||
[#passwd{password = Passwd}] when is_list(Passwd) ->
|
||||
DigRes = if
|
||||
Digest /= "" ->
|
||||
Digest == DigestGen(Passwd);
|
||||
true ->
|
||||
false
|
||||
end,
|
||||
if DigRes ->
|
||||
true;
|
||||
true ->
|
||||
(Passwd == Password) and (Password /= "")
|
||||
end;
|
||||
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||
Passwd = base64:decode(Scram#scram.storedkey),
|
||||
DigRes = if
|
||||
Digest /= "" ->
|
||||
Digest == DigestGen(Passwd);
|
||||
@ -124,8 +148,12 @@ set_password(User, Server, Password) ->
|
||||
{error, invalid_jid};
|
||||
true ->
|
||||
F = fun() ->
|
||||
Password2 = case is_scrammed() and is_list(Password) of
|
||||
true -> password_to_scram(Password);
|
||||
false -> Password
|
||||
end,
|
||||
mnesia:write(#passwd{us = US,
|
||||
password = Password})
|
||||
password = Password2})
|
||||
end,
|
||||
{atomic, ok} = mnesia:transaction(F),
|
||||
ok
|
||||
@ -143,8 +171,12 @@ try_register(User, Server, Password) ->
|
||||
F = fun() ->
|
||||
case mnesia:read({passwd, US}) of
|
||||
[] ->
|
||||
Password2 = case is_scrammed() and is_list(Password) of
|
||||
true -> password_to_scram(Password);
|
||||
false -> Password
|
||||
end,
|
||||
mnesia:write(#passwd{us = US,
|
||||
password = Password}),
|
||||
password = Password2}),
|
||||
mnesia:dirty_update_counter(
|
||||
reg_users_counter,
|
||||
LServer, 1),
|
||||
@ -239,8 +271,13 @@ get_password(User, Server) ->
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read(passwd, US) of
|
||||
[#passwd{password = Password}] ->
|
||||
[#passwd{password = Password}] when is_list(Password) ->
|
||||
Password;
|
||||
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||
{base64:decode(Scram#scram.storedkey),
|
||||
base64:decode(Scram#scram.serverkey),
|
||||
base64:decode(Scram#scram.salt),
|
||||
Scram#scram.iterationcount};
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
@ -250,8 +287,10 @@ get_password_s(User, Server) ->
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read(passwd, US) of
|
||||
[#passwd{password = Password}] ->
|
||||
[#passwd{password = Password}] when is_list(Password) ->
|
||||
Password;
|
||||
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||
[];
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
@ -293,13 +332,21 @@ remove_user(User, Server, Password) ->
|
||||
US = {LUser, LServer},
|
||||
F = fun() ->
|
||||
case mnesia:read({passwd, US}) of
|
||||
[#passwd{password = Password}] ->
|
||||
[#passwd{password = Password}] when is_list(Password) ->
|
||||
mnesia:delete({passwd, US}),
|
||||
mnesia:dirty_update_counter(reg_users_counter,
|
||||
LServer, -1),
|
||||
ok;
|
||||
[_] ->
|
||||
not_allowed;
|
||||
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||
case is_password_scram_valid(Password, Scram) of
|
||||
true ->
|
||||
mnesia:delete({passwd, US}),
|
||||
mnesia:dirty_update_counter(reg_users_counter,
|
||||
LServer, -1),
|
||||
ok;
|
||||
false ->
|
||||
not_allowed
|
||||
end;
|
||||
_ ->
|
||||
not_exists
|
||||
end
|
||||
@ -313,11 +360,11 @@ remove_user(User, Server, Password) ->
|
||||
bad_request
|
||||
end.
|
||||
|
||||
|
||||
update_table() ->
|
||||
Fields = record_info(fields, passwd),
|
||||
case mnesia:table_info(passwd, attributes) of
|
||||
Fields ->
|
||||
maybe_scram_passwords(),
|
||||
ok;
|
||||
[user, password] ->
|
||||
?INFO_MSG("Converting passwd table from "
|
||||
@ -356,5 +403,68 @@ update_table() ->
|
||||
mnesia:transform_table(passwd, ignore, Fields)
|
||||
end.
|
||||
|
||||
%%%
|
||||
%%% SCRAM
|
||||
%%%
|
||||
|
||||
%% The passwords are stored scrammed in the table either if the option says so,
|
||||
%% or if at least the first password is scrammed.
|
||||
is_scrammed() ->
|
||||
OptionScram = is_option_scram(),
|
||||
FirstElement = mnesia:dirty_read(passwd, mnesia:dirty_first(passwd)),
|
||||
case {OptionScram, FirstElement} of
|
||||
{true, _} ->
|
||||
true;
|
||||
{false, [#passwd{password = Scram}]} when is_record(Scram, scram) ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
is_option_scram() ->
|
||||
scram == ejabberd_config:get_local_option({auth_password_format, ?MYNAME}).
|
||||
|
||||
maybe_alert_password_scrammed_without_option() ->
|
||||
case is_scrammed() 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() ->
|
||||
case is_scrammed() of
|
||||
true -> scram_passwords();
|
||||
false -> ok
|
||||
end.
|
||||
|
||||
scram_passwords() ->
|
||||
?INFO_MSG("Converting the stored passwords into SCRAM bits", []),
|
||||
Fun = fun(#passwd{password = Password} = P) ->
|
||||
Scram = password_to_scram(Password),
|
||||
P#passwd{password = Scram}
|
||||
end,
|
||||
Fields = record_info(fields, passwd),
|
||||
mnesia:transform_table(passwd, Fun, Fields).
|
||||
|
||||
password_to_scram(Password) ->
|
||||
password_to_scram(Password, ?SCRAM_DEFAULT_ITERATION_COUNT).
|
||||
|
||||
password_to_scram(Password, 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),
|
||||
#scram{storedkey = base64:encode(StoredKey),
|
||||
serverkey = base64:encode(ServerKey),
|
||||
salt = base64:encode(Salt),
|
||||
iterationcount = IterationCount}.
|
||||
|
||||
is_password_scram_valid(Password, Scram) ->
|
||||
IterationCount = Scram#scram.iterationcount,
|
||||
Salt = base64:decode(Scram#scram.salt),
|
||||
SaltedPassword = scram:salted_password(Password, Salt, IterationCount),
|
||||
StoredKey = scram:stored_key(scram:client_key(SaltedPassword)),
|
||||
(base64:decode(Scram#scram.storedkey) == StoredKey).
|
||||
|
@ -1,369 +0,0 @@
|
||||
-module(ejabberd_auth_internal_scram).
|
||||
-author('stephen.roettger@googlemail.com').
|
||||
|
||||
%% External exports
|
||||
-export([start/1,
|
||||
set_password/3,
|
||||
check_password/3,
|
||||
check_password/5,
|
||||
try_register/3,
|
||||
dirty_get_registered_users/0,
|
||||
get_vh_registered_users/1,
|
||||
get_vh_registered_users/2,
|
||||
get_vh_registered_users_number/1,
|
||||
get_vh_registered_users_number/2,
|
||||
get_password/2,
|
||||
get_password_s/2,
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
storage_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
|
||||
-record(passwd, {us, stored_key, salt, iteration_count, server_key}).
|
||||
-record(reg_users_counter, {vhost, count}).
|
||||
|
||||
-define(DEFAULT_ITERATION_COUNT, 4096).
|
||||
-define(SALT_LENGTH, 16).
|
||||
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% API
|
||||
%%%----------------------------------------------------------------------
|
||||
start(Host) ->
|
||||
mnesia:create_table(passwd, [{disc_copies, [node()]},
|
||||
{attributes, record_info(fields, passwd)}]),
|
||||
mnesia:create_table(reg_users_counter,
|
||||
[{ram_copies, [node()]},
|
||||
{attributes, record_info(fields, reg_users_counter)}]),
|
||||
update_table(),
|
||||
update_reg_users_counter_table(Host),
|
||||
ok.
|
||||
|
||||
update_reg_users_counter_table(Server) ->
|
||||
Set = get_vh_registered_users(Server),
|
||||
Size = length(Set),
|
||||
LServer = jlib:nameprep(Server),
|
||||
F = fun() ->
|
||||
mnesia:write(#reg_users_counter{vhost = LServer,
|
||||
count = Size})
|
||||
end,
|
||||
mnesia:sync_dirty(F).
|
||||
|
||||
plain_password_required() ->
|
||||
true.
|
||||
|
||||
storage_type() ->
|
||||
scram.
|
||||
|
||||
check_password(User, Server, Password) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
[UserEntry] = (catch mnesia:dirty_read({passwd, US})),
|
||||
IterationCount = UserEntry#passwd.iteration_count,
|
||||
Salt = UserEntry#passwd.salt,
|
||||
SaltedPassword = scram:salted_password(Password, Salt, IterationCount),
|
||||
StoredKey = scram:stored_key(scram:client_key(SaltedPassword)),
|
||||
if
|
||||
(UserEntry#passwd.stored_key == StoredKey) ->
|
||||
true;
|
||||
true ->
|
||||
false
|
||||
end.
|
||||
|
||||
check_password(User, Server, Password, Digest, DigestGen) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read({passwd, US}) of
|
||||
[#passwd{stored_key = Passwd}] ->
|
||||
DigRes = if
|
||||
Digest /= "" ->
|
||||
Digest == DigestGen(Passwd);
|
||||
true ->
|
||||
false
|
||||
end,
|
||||
if DigRes ->
|
||||
true;
|
||||
true ->
|
||||
(Passwd == Password) and (Password /= "")
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% @spec (User::string(), Server::string(), Password::string()) ->
|
||||
%% ok | {error, invalid_jid}
|
||||
set_password(User, Server, Password) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
Salt = crypto:rand_bytes(?SALT_LENGTH),
|
||||
IterationCount = ?DEFAULT_ITERATION_COUNT,
|
||||
SaltedPassword = scram:salted_password(Password, Salt, IterationCount),
|
||||
StoredKey = scram:stored_key(scram:client_key(SaltedPassword)),
|
||||
ServerKey = scram:server_key(SaltedPassword),
|
||||
if
|
||||
(LUser == error) or (LServer == error) ->
|
||||
{error, invalid_jid};
|
||||
true ->
|
||||
F = fun() ->
|
||||
mnesia:write(#passwd{us = US,
|
||||
stored_key = StoredKey,
|
||||
salt = Salt,
|
||||
iteration_count = IterationCount,
|
||||
server_key = ServerKey})
|
||||
end,
|
||||
{atomic, ok} = mnesia:transaction(F),
|
||||
ok
|
||||
end.
|
||||
|
||||
%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} | {aborted, Reason}
|
||||
try_register(User, Server, Password) ->
|
||||
try_register(User, Server, Password, ?DEFAULT_ITERATION_COUNT).
|
||||
|
||||
try_register(User, Server, Password, IterationCount) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
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),
|
||||
if
|
||||
(LUser == error) or (LServer == error) ->
|
||||
{error, invalid_jid};
|
||||
true ->
|
||||
F = fun() ->
|
||||
case mnesia:read({passwd, US}) of
|
||||
[] ->
|
||||
mnesia:write(#passwd{us = US,
|
||||
stored_key = StoredKey,
|
||||
salt = Salt,
|
||||
iteration_count = IterationCount,
|
||||
server_key = ServerKey}),
|
||||
mnesia:dirty_update_counter(
|
||||
reg_users_counter,
|
||||
LServer, 1),
|
||||
ok;
|
||||
[_E] ->
|
||||
exists
|
||||
end
|
||||
end,
|
||||
mnesia:transaction(F)
|
||||
end.
|
||||
|
||||
%% Get all registered users in Mnesia
|
||||
dirty_get_registered_users() ->
|
||||
mnesia:dirty_all_keys(passwd).
|
||||
|
||||
get_vh_registered_users(Server) ->
|
||||
LServer = jlib:nameprep(Server),
|
||||
mnesia:dirty_select(
|
||||
passwd,
|
||||
[{#passwd{us = '$1', _ = '_'},
|
||||
[{'==', {element, 2, '$1'}, LServer}],
|
||||
['$1']}]).
|
||||
|
||||
get_vh_registered_users(Server, [{from, Start}, {to, End}])
|
||||
when is_integer(Start) and is_integer(End) ->
|
||||
get_vh_registered_users(Server, [{limit, End-Start+1}, {offset, Start}]);
|
||||
|
||||
get_vh_registered_users(Server, [{limit, Limit}, {offset, Offset}])
|
||||
when is_integer(Limit) and is_integer(Offset) ->
|
||||
case get_vh_registered_users(Server) of
|
||||
[] ->
|
||||
[];
|
||||
Users ->
|
||||
Set = lists:keysort(1, Users),
|
||||
L = length(Set),
|
||||
Start = if Offset < 1 -> 1;
|
||||
Offset > L -> L;
|
||||
true -> Offset
|
||||
end,
|
||||
lists:sublist(Set, Start, Limit)
|
||||
end;
|
||||
|
||||
get_vh_registered_users(Server, [{prefix, Prefix}])
|
||||
when is_list(Prefix) ->
|
||||
Set = [{U,S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)],
|
||||
lists:keysort(1, Set);
|
||||
|
||||
get_vh_registered_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}])
|
||||
when is_list(Prefix) and is_integer(Start) and is_integer(End) ->
|
||||
get_vh_registered_users(Server, [{prefix, Prefix}, {limit, End-Start+1}, {offset, Start}]);
|
||||
|
||||
get_vh_registered_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}])
|
||||
when is_list(Prefix) and is_integer(Limit) and is_integer(Offset) ->
|
||||
case [{U,S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)] of
|
||||
[] ->
|
||||
[];
|
||||
Users ->
|
||||
Set = lists:keysort(1, Users),
|
||||
L = length(Set),
|
||||
Start = if Offset < 1 -> 1;
|
||||
Offset > L -> L;
|
||||
true -> Offset
|
||||
end,
|
||||
lists:sublist(Set, Start, Limit)
|
||||
end;
|
||||
|
||||
get_vh_registered_users(Server, _) ->
|
||||
get_vh_registered_users(Server).
|
||||
|
||||
get_vh_registered_users_number(Server) ->
|
||||
LServer = jlib:nameprep(Server),
|
||||
Query = mnesia:dirty_select(
|
||||
reg_users_counter,
|
||||
[{#reg_users_counter{vhost = LServer, count = '$1'},
|
||||
[],
|
||||
['$1']}]),
|
||||
case Query of
|
||||
[Count] ->
|
||||
Count;
|
||||
_ -> 0
|
||||
end.
|
||||
|
||||
get_vh_registered_users_number(Server, [{prefix, Prefix}]) when is_list(Prefix) ->
|
||||
Set = [{U, S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)],
|
||||
length(Set);
|
||||
|
||||
get_vh_registered_users_number(Server, _) ->
|
||||
get_vh_registered_users_number(Server).
|
||||
|
||||
get_password(User, Server) ->
|
||||
%false.
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read(passwd, US) of
|
||||
[#passwd{stored_key = Password, server_key = ServerKey, salt = Salt, iteration_count = IterationCount}] ->
|
||||
{Password, ServerKey, Salt, IterationCount};
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
get_password_s(User, Server) ->
|
||||
%"".
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read(passwd, US) of
|
||||
[#passwd{stored_key = Password, server_key = ServerKey, salt = Salt, iteration_count = IterationCount}] ->
|
||||
{Password, ServerKey, Salt, IterationCount};
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
%% @spec (User, Server) -> true | false | {error, Error}
|
||||
is_user_exists(User, Server) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
case catch mnesia:dirty_read({passwd, US}) of
|
||||
[] ->
|
||||
false;
|
||||
[_] ->
|
||||
true;
|
||||
Other ->
|
||||
{error, Other}
|
||||
end.
|
||||
|
||||
%% @spec (User, Server) -> ok
|
||||
%% @doc Remove user.
|
||||
%% Note: it returns ok even if there was some problem removing the user.
|
||||
remove_user(User, Server) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
F = fun() ->
|
||||
mnesia:delete({passwd, US}),
|
||||
mnesia:dirty_update_counter(reg_users_counter,
|
||||
LServer, -1)
|
||||
end,
|
||||
mnesia:transaction(F),
|
||||
ok.
|
||||
|
||||
%% @spec (User, Server, Password) -> ok | not_exists | not_allowed | bad_request
|
||||
%% @doc Remove user if the provided password is correct.
|
||||
remove_user(User, Server, Password) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
US = {LUser, LServer},
|
||||
F = fun() ->
|
||||
case mnesia:read({passwd, US}) of
|
||||
[UserEntry] ->
|
||||
IterationCount = UserEntry#passwd.iteration_count,
|
||||
Salt = UserEntry#passwd.salt,
|
||||
SaltedPassword = scram:salted_password(Password, Salt, IterationCount),
|
||||
StoredKey = scram:stored_key(scram:client_key(SaltedPassword)),
|
||||
if
|
||||
(UserEntry#passwd.stored_key == StoredKey) ->
|
||||
mnesia:delete({passwd, US}),
|
||||
mnesia:dirty_update_counter(reg_users_counter,
|
||||
LServer, -1),
|
||||
ok;
|
||||
true ->
|
||||
not_allowed
|
||||
end;
|
||||
_Else ->
|
||||
not_exists
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, ok} ->
|
||||
ok;
|
||||
{atomic, Res} ->
|
||||
Res;
|
||||
_ ->
|
||||
bad_request
|
||||
end.
|
||||
|
||||
|
||||
update_table() ->
|
||||
Fields = record_info(fields, passwd),
|
||||
case mnesia:table_info(passwd, attributes) of
|
||||
Fields ->
|
||||
ok;
|
||||
[user, stored_key] ->
|
||||
?INFO_MSG("Converting passwd table from "
|
||||
"{user, stored_key} format", []),
|
||||
Host = ?MYNAME,
|
||||
{atomic, ok} = mnesia:create_table(
|
||||
ejabberd_auth_internal_tmp_table,
|
||||
[{disc_only_copies, [node()]},
|
||||
{type, bag},
|
||||
{local_content, true},
|
||||
{record_name, passwd},
|
||||
{attributes, record_info(fields, passwd)}]),
|
||||
mnesia:transform_table(passwd, ignore, Fields),
|
||||
F1 = fun() ->
|
||||
mnesia:write_lock_table(ejabberd_auth_internal_tmp_table),
|
||||
mnesia:foldl(
|
||||
fun(#passwd{us = U} = R, _) ->
|
||||
mnesia:dirty_write(
|
||||
ejabberd_auth_internal_tmp_table,
|
||||
R#passwd{us = {U, Host}})
|
||||
end, ok, passwd)
|
||||
end,
|
||||
mnesia:transaction(F1),
|
||||
mnesia:clear_table(passwd),
|
||||
F2 = fun() ->
|
||||
mnesia:write_lock_table(passwd),
|
||||
mnesia:foldl(
|
||||
fun(R, _) ->
|
||||
mnesia:dirty_write(R)
|
||||
end, ok, ejabberd_auth_internal_tmp_table)
|
||||
end,
|
||||
mnesia:transaction(F2),
|
||||
mnesia:delete_table(ejabberd_auth_internal_tmp_table);
|
||||
_ ->
|
||||
?INFO_MSG("Recreating passwd table", []),
|
||||
mnesia:transform_table(passwd, ignore, Fields)
|
||||
end.
|
||||
|
||||
|
||||
|
@ -54,7 +54,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
storage_type/0,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -138,7 +138,7 @@ init(Host) ->
|
||||
plain_password_required() ->
|
||||
true.
|
||||
|
||||
storage_type() ->
|
||||
store_type() ->
|
||||
external.
|
||||
|
||||
check_password(User, Server, Password) ->
|
||||
|
@ -43,7 +43,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
storage_type/0,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -58,7 +58,7 @@ start(_Host) ->
|
||||
plain_password_required() ->
|
||||
false.
|
||||
|
||||
storage_type() ->
|
||||
store_type() ->
|
||||
plain.
|
||||
|
||||
%% @spec (User, Server, Password) -> true | false | {error, Error}
|
||||
|
@ -39,7 +39,7 @@
|
||||
is_user_exists/2,
|
||||
remove_user/2,
|
||||
remove_user/3,
|
||||
storage_type/0,
|
||||
store_type/0,
|
||||
plain_password_required/0
|
||||
]).
|
||||
|
||||
@ -107,7 +107,7 @@ remove_user(_User, _Server, _Password) ->
|
||||
plain_password_required() ->
|
||||
true.
|
||||
|
||||
storage_type() ->
|
||||
store_type() ->
|
||||
external.
|
||||
|
||||
%%====================================================================
|
||||
|
@ -159,13 +159,13 @@ process_element(El=#xmlel{name=user, ns=_XMLNS},
|
||||
State;
|
||||
|
||||
process_element(H=#xmlel{name=host},State) ->
|
||||
State#parsing_state{host=?BTL(exmpp_xml:get_attribute(H,"jid",none))};
|
||||
State#parsing_state{host=?BTL(exmpp_xml:get_attribute(H, <<"jid">>, none))};
|
||||
|
||||
process_element(#xmlel{name='server-data'},State) ->
|
||||
State;
|
||||
|
||||
process_element(El=#xmlel{name=include, ns=?NS_XINCLUDE}, State=#parsing_state{dir=Dir}) ->
|
||||
case exmpp_xml:get_attribute(El, href, none) of
|
||||
case exmpp_xml:get_attribute(El, <<"href">>, none) of
|
||||
none ->
|
||||
ok;
|
||||
HrefB ->
|
||||
@ -194,25 +194,26 @@ process_element(El,State) ->
|
||||
%%%% Add user
|
||||
|
||||
add_user(El, Domain) ->
|
||||
User = exmpp_xml:get_attribute(El,name,none),
|
||||
Password = exmpp_xml:get_attribute(El,password,none),
|
||||
add_user(El, Domain, User, Password).
|
||||
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, PasswordFormat, Password).
|
||||
|
||||
%% @spec (El::xmlel(), Domain::string(), User::binary(), Password::binary() | none)
|
||||
%% -> ok | {error, ErrorText::string()}
|
||||
%% @doc Add a new user to the database.
|
||||
%% If user already exists, it will be only updated.
|
||||
add_user(El, Domain, UserBinary, none) ->
|
||||
add_user(El, Domain, UserBinary, <<"plaintext">>, none) ->
|
||||
User = ?BTL(UserBinary),
|
||||
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, UserBinary, PasswordBinary) ->
|
||||
add_user(El, Domain, UserBinary, PasswordFormat, PasswordBinary) ->
|
||||
User = ?BTL(UserBinary),
|
||||
Password = ?BTL(PasswordBinary),
|
||||
case create_user(User,Password,Domain) of
|
||||
Password2 = prepare_password(PasswordFormat, PasswordBinary, El),
|
||||
case create_user(User,Password2,Domain) of
|
||||
ok ->
|
||||
populate_user_with_elements(El, Domain, User),
|
||||
ok;
|
||||
@ -227,6 +228,21 @@ add_user(El, Domain, UserBinary, PasswordBinary) ->
|
||||
{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) ->
|
||||
@ -346,7 +362,7 @@ populate_user(User,Domain,El=#xmlel{name='offline-messages'}) ->
|
||||
fun (_Element, {xmlcdata, _}) ->
|
||||
ok;
|
||||
(_Element, Child) ->
|
||||
From = exmpp_xml:get_attribute(Child,from,none),
|
||||
From = exmpp_xml:get_attribute(Child, <<"from">>,none),
|
||||
FullFrom = jid_to_old_jid(exmpp_jid:parse(From)),
|
||||
FullUser = jid_to_old_jid(exmpp_jid:make(User,
|
||||
Domain)),
|
||||
@ -538,10 +554,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) ->
|
||||
|
Loading…
Reference in New Issue
Block a user