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.
|
\ejabberd{} uses its internal Mnesia database as the default authentication method.
|
||||||
The value \term{internal} will enable the internal 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:
|
Examples:
|
||||||
\begin{itemize}
|
\begin{itemize}
|
||||||
\item To use internal authentication on \jid{example.org} and LDAP
|
\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.org", [{auth_method, [internal]}]}.
|
||||||
{host_config, "example.net", [{auth_method, [ldap]}]}.
|
{host_config, "example.net", [{auth_method, [ldap]}]}.
|
||||||
\end{verbatim}
|
\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}
|
\begin{verbatim}
|
||||||
{auth_method, internal}.
|
{auth_method, internal}.
|
||||||
|
{auth_password_format, scram}.
|
||||||
\end{verbatim}
|
\end{verbatim}
|
||||||
\end{itemize}
|
\end{itemize}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@
|
||||||
server_start/3,
|
server_start/3,
|
||||||
server_step/2]).
|
server_step/2]).
|
||||||
|
|
||||||
|
-include("ejabberd.hrl").
|
||||||
|
|
||||||
-record(sasl_mechanism, {mechanism, module, password_type}).
|
-record(sasl_mechanism, {mechanism, module, password_type}).
|
||||||
-record(sasl_state, {service, myname, realm,
|
-record(sasl_state, {service, myname, realm,
|
||||||
get_password, check_password, check_password_digest,
|
get_password, check_password, check_password_digest,
|
||||||
|
@ -102,11 +104,14 @@ listmech(Host) ->
|
||||||
[{#sasl_mechanism{mechanism = '$1',
|
[{#sasl_mechanism{mechanism = '$1',
|
||||||
password_type = '$2',
|
password_type = '$2',
|
||||||
_ = '_'},
|
_ = '_'},
|
||||||
case ejabberd_auth:storage_type(Host) of
|
case catch ejabberd_auth:store_type(Host) of
|
||||||
external ->
|
external ->
|
||||||
[{'==', '$2', plain}];
|
[{'==', '$2', plain}];
|
||||||
scram ->
|
scram ->
|
||||||
[{'/=', '$2', digest}];
|
[{'/=', '$2', digest}];
|
||||||
|
{'EXIT',{undef,[{Module,store_type,[]} | _]}} ->
|
||||||
|
?WARNING_MSG("~p doesn't implement the function store_type/0", [Module]),
|
||||||
|
[];
|
||||||
_Else ->
|
_Else ->
|
||||||
[]
|
[]
|
||||||
end,
|
end,
|
||||||
|
|
|
@ -39,7 +39,6 @@
|
||||||
-record(state, {step, stored_key, server_key, username, get_password, check_password,
|
-record(state, {step, stored_key, server_key, username, get_password, check_password,
|
||||||
auth_message, client_nonce, server_nonce}).
|
auth_message, client_nonce, server_nonce}).
|
||||||
|
|
||||||
-define(DEFAULT_ITERATION_COUNT, 4096).
|
|
||||||
-define(SALT_LENGTH, 16).
|
-define(SALT_LENGTH, 16).
|
||||||
-define(NONCE_LENGTH, 16).
|
-define(NONCE_LENGTH, 16).
|
||||||
|
|
||||||
|
@ -54,9 +53,9 @@ mech_new(_Host, GetPassword, _CheckPassword, _CheckPasswordDigest) ->
|
||||||
|
|
||||||
mech_step(#state{step = 2} = State, ClientIn) ->
|
mech_step(#state{step = 2} = State, ClientIn) ->
|
||||||
case string:tokens(ClientIn, ",") of
|
case string:tokens(ClientIn, ",") of
|
||||||
["n", UserNameAttribute, ClientNonceAttribute] ->
|
[CBind, UserNameAttribute, ClientNonceAttribute] when (CBind == "y") or (CBind == "n") ->
|
||||||
case parse_attribute(UserNameAttribute) of
|
case parse_attribute(UserNameAttribute) of
|
||||||
{$n, EscapedUserName} ->
|
{_, EscapedUserName} ->
|
||||||
case unescape_username(EscapedUserName) of
|
case unescape_username(EscapedUserName) of
|
||||||
error ->
|
error ->
|
||||||
{error, "protocol-error-bad-username"};
|
{error, "protocol-error-bad-username"};
|
||||||
|
@ -72,8 +71,9 @@ mech_step(#state{step = 2} = State, ClientIn) ->
|
||||||
Ret;
|
Ret;
|
||||||
true ->
|
true ->
|
||||||
TempSalt = crypto:rand_bytes(?SALT_LENGTH),
|
TempSalt = crypto:rand_bytes(?SALT_LENGTH),
|
||||||
SaltedPassword = scram:salted_password(Ret, TempSalt, ?DEFAULT_ITERATION_COUNT),
|
SaltedPassword = scram:salted_password(Ret, TempSalt, ?SCRAM_DEFAULT_ITERATION_COUNT),
|
||||||
{scram:stored_key(scram:client_key(SaltedPassword)), TempSalt, ?DEFAULT_ITERATION_COUNT}
|
{scram:stored_key(scram:client_key(SaltedPassword)),
|
||||||
|
scram:server_key(SaltedPassword), TempSalt, ?SCRAM_DEFAULT_ITERATION_COUNT}
|
||||||
end,
|
end,
|
||||||
ClientFirstMessageBare = string:substr(ClientIn, string:str(ClientIn, "n=")),
|
ClientFirstMessageBare = string:substr(ClientIn, string:str(ClientIn, "n=")),
|
||||||
ServerNonce = base64:encode_to_string(crypto:rand_bytes(?NONCE_LENGTH)),
|
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
|
case string:tokens(ClientIn, ",") of
|
||||||
[GS2ChannelBindingAttribute, NonceAttribute, ClientProofAttribute] ->
|
[GS2ChannelBindingAttribute, NonceAttribute, ClientProofAttribute] ->
|
||||||
case parse_attribute(GS2ChannelBindingAttribute) of
|
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,
|
Nonce = State#state.client_nonce ++ State#state.server_nonce,
|
||||||
case parse_attribute(NonceAttribute) of
|
case parse_attribute(NonceAttribute) of
|
||||||
{$r, CompareNonce} when CompareNonce == Nonce ->
|
{$r, CompareNonce} when CompareNonce == Nonce ->
|
||||||
|
|
|
@ -218,6 +218,10 @@
|
||||||
%% comment this line and enable the correct ones.
|
%% comment this line and enable the correct ones.
|
||||||
%%
|
%%
|
||||||
{auth_method, internal}.
|
{auth_method, internal}.
|
||||||
|
%%
|
||||||
|
%% Store the plain passwords or hashed for SCRAM:
|
||||||
|
%%{auth_password_format, plain}.
|
||||||
|
%%{auth_password_format, scram}.
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%% Authentication using external script
|
%% Authentication using external script
|
||||||
|
|
|
@ -37,6 +37,9 @@
|
||||||
|
|
||||||
%%-define(DBGFSM, true).
|
%%-define(DBGFSM, true).
|
||||||
|
|
||||||
|
-record(scram, {storedkey, serverkey, salt, iterationcount}).
|
||||||
|
-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096).
|
||||||
|
|
||||||
%% ---------------------------------
|
%% ---------------------------------
|
||||||
%% Logging mechanism
|
%% Logging mechanism
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
remove_user/3,
|
remove_user/3,
|
||||||
plain_password_required/1,
|
plain_password_required/1,
|
||||||
storage_type/1,
|
store_type/1,
|
||||||
entropy/1
|
entropy/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -70,25 +70,26 @@ start() ->
|
||||||
end, auth_modules(Host))
|
end, auth_modules(Host))
|
||||||
end, ?MYHOSTS).
|
end, ?MYHOSTS).
|
||||||
|
|
||||||
|
%% This is only executed by ejabberd_c2s for non-SASL auth client
|
||||||
plain_password_required(Server) ->
|
plain_password_required(Server) ->
|
||||||
lists:any(
|
lists:any(
|
||||||
fun(M) ->
|
fun(M) ->
|
||||||
M:plain_password_required()
|
M:plain_password_required()
|
||||||
end, auth_modules(Server)).
|
end, auth_modules(Server)).
|
||||||
|
|
||||||
storage_type(Server) ->
|
store_type(Server) ->
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun(_, external) ->
|
fun(_, external) ->
|
||||||
external;
|
external;
|
||||||
(M, scram) ->
|
(M, scram) ->
|
||||||
case M:storage_type() of
|
case M:store_type() of
|
||||||
external ->
|
external ->
|
||||||
external;
|
external;
|
||||||
_Else ->
|
_Else ->
|
||||||
scram
|
scram
|
||||||
end;
|
end;
|
||||||
(M, plain) ->
|
(M, plain) ->
|
||||||
M:storage_type()
|
M:store_type()
|
||||||
end, plain, auth_modules(Server)).
|
end, plain, auth_modules(Server)).
|
||||||
|
|
||||||
%% @doc Check if the user and password can login in 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
|
case get_password(User, Server) of
|
||||||
false ->
|
false ->
|
||||||
"";
|
"";
|
||||||
Password ->
|
Password when is_list(Password) ->
|
||||||
Password
|
Password;
|
||||||
|
_ ->
|
||||||
|
""
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Get the password of the user and the auth module.
|
%% @doc Get the password of the user and the auth module.
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
is_user_exists/2,
|
is_user_exists/2,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
remove_user/3,
|
remove_user/3,
|
||||||
storage_type/0,
|
store_type/0,
|
||||||
plain_password_required/0]).
|
plain_password_required/0]).
|
||||||
|
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
|
@ -249,5 +249,5 @@ remove_user(_User, _Server, _Password) ->
|
||||||
plain_password_required() ->
|
plain_password_required() ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
storage_type() ->
|
store_type() ->
|
||||||
plain.
|
plain.
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
is_user_exists/2,
|
is_user_exists/2,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
remove_user/3,
|
remove_user/3,
|
||||||
storage_type/0,
|
store_type/0,
|
||||||
plain_password_required/0
|
plain_password_required/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ check_cache_last_options(Server) ->
|
||||||
plain_password_required() ->
|
plain_password_required() ->
|
||||||
true.
|
true.
|
||||||
|
|
||||||
storage_type() ->
|
store_type() ->
|
||||||
external.
|
external.
|
||||||
|
|
||||||
check_password(User, Server, Password) ->
|
check_password(User, Server, Password) ->
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
is_user_exists/2,
|
is_user_exists/2,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
remove_user/3,
|
remove_user/3,
|
||||||
storage_type/0,
|
store_type/0,
|
||||||
plain_password_required/0
|
plain_password_required/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -52,6 +52,8 @@
|
||||||
-record(passwd, {us, password}).
|
-record(passwd, {us, password}).
|
||||||
-record(reg_users_counter, {vhost, count}).
|
-record(reg_users_counter, {vhost, count}).
|
||||||
|
|
||||||
|
-define(SALT_LENGTH, 16).
|
||||||
|
|
||||||
%%%----------------------------------------------------------------------
|
%%%----------------------------------------------------------------------
|
||||||
%%% API
|
%%% API
|
||||||
%%%----------------------------------------------------------------------
|
%%%----------------------------------------------------------------------
|
||||||
|
@ -63,6 +65,7 @@ start(Host) ->
|
||||||
{attributes, record_info(fields, reg_users_counter)}]),
|
{attributes, record_info(fields, reg_users_counter)}]),
|
||||||
update_table(),
|
update_table(),
|
||||||
update_reg_users_counter_table(Host),
|
update_reg_users_counter_table(Host),
|
||||||
|
maybe_alert_password_scrammed_without_option(),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
update_reg_users_counter_table(Server) ->
|
update_reg_users_counter_table(Server) ->
|
||||||
|
@ -76,18 +79,26 @@ update_reg_users_counter_table(Server) ->
|
||||||
mnesia:sync_dirty(F).
|
mnesia:sync_dirty(F).
|
||||||
|
|
||||||
plain_password_required() ->
|
plain_password_required() ->
|
||||||
false.
|
case is_scrammed() of
|
||||||
|
false -> false;
|
||||||
|
true -> true
|
||||||
|
end.
|
||||||
|
|
||||||
storage_type() ->
|
store_type() ->
|
||||||
plain.
|
case is_scrammed() of
|
||||||
|
false -> plain; %% allows: PLAIN DIGEST-MD5 SCRAM
|
||||||
|
true -> scram %% allows: PLAIN SCRAM
|
||||||
|
end.
|
||||||
|
|
||||||
check_password(User, Server, Password) ->
|
check_password(User, Server, Password) ->
|
||||||
LUser = jlib:nodeprep(User),
|
LUser = jlib:nodeprep(User),
|
||||||
LServer = jlib:nameprep(Server),
|
LServer = jlib:nameprep(Server),
|
||||||
US = {LUser, LServer},
|
US = {LUser, LServer},
|
||||||
case catch mnesia:dirty_read({passwd, US}) of
|
case catch mnesia:dirty_read({passwd, US}) of
|
||||||
[#passwd{password = Password}] ->
|
[#passwd{password = Password}] when is_list(Password) ->
|
||||||
Password /= "";
|
Password /= "";
|
||||||
|
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||||
|
is_password_scram_valid(Password, Scram);
|
||||||
_ ->
|
_ ->
|
||||||
false
|
false
|
||||||
end.
|
end.
|
||||||
|
@ -97,7 +108,20 @@ check_password(User, Server, Password, Digest, DigestGen) ->
|
||||||
LServer = jlib:nameprep(Server),
|
LServer = jlib:nameprep(Server),
|
||||||
US = {LUser, LServer},
|
US = {LUser, LServer},
|
||||||
case catch mnesia:dirty_read({passwd, US}) of
|
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
|
DigRes = if
|
||||||
Digest /= "" ->
|
Digest /= "" ->
|
||||||
Digest == DigestGen(Passwd);
|
Digest == DigestGen(Passwd);
|
||||||
|
@ -124,8 +148,12 @@ set_password(User, Server, Password) ->
|
||||||
{error, invalid_jid};
|
{error, invalid_jid};
|
||||||
true ->
|
true ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
|
Password2 = case is_scrammed() and is_list(Password) of
|
||||||
|
true -> password_to_scram(Password);
|
||||||
|
false -> Password
|
||||||
|
end,
|
||||||
mnesia:write(#passwd{us = US,
|
mnesia:write(#passwd{us = US,
|
||||||
password = Password})
|
password = Password2})
|
||||||
end,
|
end,
|
||||||
{atomic, ok} = mnesia:transaction(F),
|
{atomic, ok} = mnesia:transaction(F),
|
||||||
ok
|
ok
|
||||||
|
@ -143,8 +171,12 @@ try_register(User, Server, Password) ->
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read({passwd, US}) of
|
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,
|
mnesia:write(#passwd{us = US,
|
||||||
password = Password}),
|
password = Password2}),
|
||||||
mnesia:dirty_update_counter(
|
mnesia:dirty_update_counter(
|
||||||
reg_users_counter,
|
reg_users_counter,
|
||||||
LServer, 1),
|
LServer, 1),
|
||||||
|
@ -239,8 +271,13 @@ get_password(User, Server) ->
|
||||||
LServer = jlib:nameprep(Server),
|
LServer = jlib:nameprep(Server),
|
||||||
US = {LUser, LServer},
|
US = {LUser, LServer},
|
||||||
case catch mnesia:dirty_read(passwd, US) of
|
case catch mnesia:dirty_read(passwd, US) of
|
||||||
[#passwd{password = Password}] ->
|
[#passwd{password = Password}] when is_list(Password) ->
|
||||||
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
|
false
|
||||||
end.
|
end.
|
||||||
|
@ -250,8 +287,10 @@ get_password_s(User, Server) ->
|
||||||
LServer = jlib:nameprep(Server),
|
LServer = jlib:nameprep(Server),
|
||||||
US = {LUser, LServer},
|
US = {LUser, LServer},
|
||||||
case catch mnesia:dirty_read(passwd, US) of
|
case catch mnesia:dirty_read(passwd, US) of
|
||||||
[#passwd{password = Password}] ->
|
[#passwd{password = Password}] when is_list(Password) ->
|
||||||
Password;
|
Password;
|
||||||
|
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||||
|
[];
|
||||||
_ ->
|
_ ->
|
||||||
[]
|
[]
|
||||||
end.
|
end.
|
||||||
|
@ -293,13 +332,21 @@ remove_user(User, Server, Password) ->
|
||||||
US = {LUser, LServer},
|
US = {LUser, LServer},
|
||||||
F = fun() ->
|
F = fun() ->
|
||||||
case mnesia:read({passwd, US}) of
|
case mnesia:read({passwd, US}) of
|
||||||
[#passwd{password = Password}] ->
|
[#passwd{password = Password}] when is_list(Password) ->
|
||||||
mnesia:delete({passwd, US}),
|
mnesia:delete({passwd, US}),
|
||||||
mnesia:dirty_update_counter(reg_users_counter,
|
mnesia:dirty_update_counter(reg_users_counter,
|
||||||
LServer, -1),
|
LServer, -1),
|
||||||
ok;
|
ok;
|
||||||
[_] ->
|
[#passwd{password = Scram}] when is_record(Scram, scram) ->
|
||||||
not_allowed;
|
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
|
not_exists
|
||||||
end
|
end
|
||||||
|
@ -313,11 +360,11 @@ remove_user(User, Server, Password) ->
|
||||||
bad_request
|
bad_request
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
update_table() ->
|
update_table() ->
|
||||||
Fields = record_info(fields, passwd),
|
Fields = record_info(fields, passwd),
|
||||||
case mnesia:table_info(passwd, attributes) of
|
case mnesia:table_info(passwd, attributes) of
|
||||||
Fields ->
|
Fields ->
|
||||||
|
maybe_scram_passwords(),
|
||||||
ok;
|
ok;
|
||||||
[user, password] ->
|
[user, password] ->
|
||||||
?INFO_MSG("Converting passwd table from "
|
?INFO_MSG("Converting passwd table from "
|
||||||
|
@ -356,5 +403,68 @@ update_table() ->
|
||||||
mnesia:transform_table(passwd, ignore, Fields)
|
mnesia:transform_table(passwd, ignore, Fields)
|
||||||
end.
|
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,
|
is_user_exists/2,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
remove_user/3,
|
remove_user/3,
|
||||||
storage_type/0,
|
store_type/0,
|
||||||
plain_password_required/0
|
plain_password_required/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ init(Host) ->
|
||||||
plain_password_required() ->
|
plain_password_required() ->
|
||||||
true.
|
true.
|
||||||
|
|
||||||
storage_type() ->
|
store_type() ->
|
||||||
external.
|
external.
|
||||||
|
|
||||||
check_password(User, Server, Password) ->
|
check_password(User, Server, Password) ->
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
is_user_exists/2,
|
is_user_exists/2,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
remove_user/3,
|
remove_user/3,
|
||||||
storage_type/0,
|
store_type/0,
|
||||||
plain_password_required/0
|
plain_password_required/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ start(_Host) ->
|
||||||
plain_password_required() ->
|
plain_password_required() ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
storage_type() ->
|
store_type() ->
|
||||||
plain.
|
plain.
|
||||||
|
|
||||||
%% @spec (User, Server, Password) -> true | false | {error, Error}
|
%% @spec (User, Server, Password) -> true | false | {error, Error}
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
is_user_exists/2,
|
is_user_exists/2,
|
||||||
remove_user/2,
|
remove_user/2,
|
||||||
remove_user/3,
|
remove_user/3,
|
||||||
storage_type/0,
|
store_type/0,
|
||||||
plain_password_required/0
|
plain_password_required/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ remove_user(_User, _Server, _Password) ->
|
||||||
plain_password_required() ->
|
plain_password_required() ->
|
||||||
true.
|
true.
|
||||||
|
|
||||||
storage_type() ->
|
store_type() ->
|
||||||
external.
|
external.
|
||||||
|
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
|
|
|
@ -159,13 +159,13 @@ process_element(El=#xmlel{name=user, ns=_XMLNS},
|
||||||
State;
|
State;
|
||||||
|
|
||||||
process_element(H=#xmlel{name=host},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) ->
|
process_element(#xmlel{name='server-data'},State) ->
|
||||||
State;
|
State;
|
||||||
|
|
||||||
process_element(El=#xmlel{name=include, ns=?NS_XINCLUDE}, State=#parsing_state{dir=Dir}) ->
|
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 ->
|
none ->
|
||||||
ok;
|
ok;
|
||||||
HrefB ->
|
HrefB ->
|
||||||
|
@ -194,25 +194,26 @@ process_element(El,State) ->
|
||||||
%%%% Add user
|
%%%% Add user
|
||||||
|
|
||||||
add_user(El, Domain) ->
|
add_user(El, Domain) ->
|
||||||
User = exmpp_xml:get_attribute(El,name,none),
|
User = exmpp_xml:get_attribute(El, <<"name">>, none),
|
||||||
Password = exmpp_xml:get_attribute(El,password,none),
|
PasswordFormat = exmpp_xml:get_attribute(El, <<"password-format">>, none),
|
||||||
add_user(El, Domain, User, Password).
|
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)
|
%% @spec (El::xmlel(), Domain::string(), User::binary(), Password::binary() | none)
|
||||||
%% -> ok | {error, ErrorText::string()}
|
%% -> ok | {error, ErrorText::string()}
|
||||||
%% @doc Add a new user to the database.
|
%% @doc Add a new user to the database.
|
||||||
%% If user already exists, it will be only updated.
|
%% 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),
|
User = ?BTL(UserBinary),
|
||||||
io:format("Account ~s@~s will not be created, updating it...~n",
|
io:format("Account ~s@~s will not be created, updating it...~n",
|
||||||
[User, Domain]),
|
[User, Domain]),
|
||||||
io:format(""),
|
io:format(""),
|
||||||
populate_user_with_elements(El, Domain, User),
|
populate_user_with_elements(El, Domain, User),
|
||||||
ok;
|
ok;
|
||||||
add_user(El, Domain, UserBinary, PasswordBinary) ->
|
add_user(El, Domain, UserBinary, PasswordFormat, PasswordBinary) ->
|
||||||
User = ?BTL(UserBinary),
|
User = ?BTL(UserBinary),
|
||||||
Password = ?BTL(PasswordBinary),
|
Password2 = prepare_password(PasswordFormat, PasswordBinary, El),
|
||||||
case create_user(User,Password,Domain) of
|
case create_user(User,Password2,Domain) of
|
||||||
ok ->
|
ok ->
|
||||||
populate_user_with_elements(El, Domain, User),
|
populate_user_with_elements(El, Domain, User),
|
||||||
ok;
|
ok;
|
||||||
|
@ -227,6 +228,21 @@ add_user(El, Domain, UserBinary, PasswordBinary) ->
|
||||||
{error, Other}
|
{error, Other}
|
||||||
end.
|
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) ->
|
populate_user_with_elements(El, Domain, User) ->
|
||||||
exmpp_xml:foreach(
|
exmpp_xml:foreach(
|
||||||
fun (_,Child) ->
|
fun (_,Child) ->
|
||||||
|
@ -346,7 +362,7 @@ populate_user(User,Domain,El=#xmlel{name='offline-messages'}) ->
|
||||||
fun (_Element, {xmlcdata, _}) ->
|
fun (_Element, {xmlcdata, _}) ->
|
||||||
ok;
|
ok;
|
||||||
(_Element, Child) ->
|
(_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)),
|
FullFrom = jid_to_old_jid(exmpp_jid:parse(From)),
|
||||||
FullUser = jid_to_old_jid(exmpp_jid:make(User,
|
FullUser = jid_to_old_jid(exmpp_jid:make(User,
|
||||||
Domain)),
|
Domain)),
|
||||||
|
@ -538,10 +554,23 @@ export_user(Fd, Username, Host) ->
|
||||||
|
|
||||||
%% @spec (Username::string(), Host::string()) -> string()
|
%% @spec (Username::string(), Host::string()) -> string()
|
||||||
extract_user(Username, Host) ->
|
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]],
|
UserInfo = [extract_user_info(InfoName, Username, Host) || InfoName <- [roster, offline, private, vcard]],
|
||||||
UserInfoString = lists:flatten(UserInfo),
|
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()
|
%% @spec (InfoName::atom(), Username::string(), Host::string()) -> string()
|
||||||
extract_user_info(roster, Username, Host) ->
|
extract_user_info(roster, Username, Host) ->
|
||||||
|
|
Loading…
Reference in New Issue