From 1ee6eae684c126b486c70a3cadd58fa282f517ad Mon Sep 17 00:00:00 2001 From: Badlop Date: Tue, 16 Aug 2011 00:26:49 +0200 Subject: [PATCH] Fix and improve support for SCRAM auth method (EJAB-1196) --- doc/guide.tex | 21 +- src/cyrsasl.erl | 7 +- src/cyrsasl_scram.erl | 14 +- src/ejabberd.cfg.example | 4 + src/ejabberd.hrl | 3 + src/ejabberd_auth.erl | 15 +- src/ejabberd_auth_anonymous.erl | 4 +- src/ejabberd_auth_external.erl | 4 +- src/ejabberd_auth_internal.erl | 138 +++++++++- src/ejabberd_auth_internal_scram.erl | 369 --------------------------- src/ejabberd_auth_ldap.erl | 4 +- src/ejabberd_auth_odbc.erl | 4 +- src/ejabberd_auth_pam.erl | 4 +- src/ejabberd_piefxis.erl | 53 +++- 14 files changed, 225 insertions(+), 419 deletions(-) delete mode 100644 src/ejabberd_auth_internal_scram.erl diff --git a/doc/guide.tex b/doc/guide.tex index 5939a9eec..49cfd5bf8 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -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} diff --git a/src/cyrsasl.erl b/src/cyrsasl.erl index 627ee4d32..28098f763 100644 --- a/src/cyrsasl.erl +++ b/src/cyrsasl.erl @@ -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, diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl index f0649f651..270cce26f 100644 --- a/src/cyrsasl_scram.erl +++ b/src/cyrsasl_scram.erl @@ -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 -> diff --git a/src/ejabberd.cfg.example b/src/ejabberd.cfg.example index 92e78e32d..57eab41bd 100644 --- a/src/ejabberd.cfg.example +++ b/src/ejabberd.cfg.example @@ -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 diff --git a/src/ejabberd.hrl b/src/ejabberd.hrl index b1bf516cd..9f8978be8 100644 --- a/src/ejabberd.hrl +++ b/src/ejabberd.hrl @@ -37,6 +37,9 @@ %%-define(DBGFSM, true). +-record(scram, {storedkey, serverkey, salt, iterationcount}). +-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096). + %% --------------------------------- %% Logging mechanism diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index d937340f1..1eab9af83 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -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. diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index d3ecffc1b..5295ab99f 100644 --- a/src/ejabberd_auth_anonymous.erl +++ b/src/ejabberd_auth_anonymous.erl @@ -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. diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index b625af84c..c4208b09e 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -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) -> diff --git a/src/ejabberd_auth_internal.erl b/src/ejabberd_auth_internal.erl index 847737f29..b196d3526 100644 --- a/src/ejabberd_auth_internal.erl +++ b/src/ejabberd_auth_internal.erl @@ -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). diff --git a/src/ejabberd_auth_internal_scram.erl b/src/ejabberd_auth_internal_scram.erl deleted file mode 100644 index f39ee4fb1..000000000 --- a/src/ejabberd_auth_internal_scram.erl +++ /dev/null @@ -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. - - - diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index da928599b..77216a1cd 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -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) -> diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl index 60e6b8059..e2d43a449 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_odbc.erl @@ -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} diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index a00d9e816..3ff59016c 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -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. %%==================================================================== diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 02697163b..cf5178bbe 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -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("~s", [Username, Password, UserInfoString]). + io_lib:format("", + [Username, PasswordStr, UserInfoString]). + +build_password_string({StoredKey, ServerKey, Salt, IterationCount}) -> + io_lib:format("password-format='scram'>" + " ", + [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) ->