Fix and improve support for SCRAM auth method (EJAB-1196)

This commit is contained in:
Badlop 2011-08-16 00:26:49 +02:00
parent 24852b9be8
commit 1ee6eae684
14 changed files with 225 additions and 419 deletions

View File

@ -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}

View File

@ -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,

View File

@ -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 ->

View File

@ -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

View File

@ -37,6 +37,9 @@
%%-define(DBGFSM, true).
-record(scram, {storedkey, serverkey, salt, iterationcount}).
-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096).
%% ---------------------------------
%% Logging mechanism

View File

@ -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.

View File

@ -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.

View File

@ -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) ->

View File

@ -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).

View File

@ -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.

View File

@ -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) ->

View File

@ -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}

View File

@ -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.
%%====================================================================

View File

@ -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) ->