diff --git a/include/ejabberd_auth.hrl b/include/ejabberd_auth.hrl index 1f9117efb..bf7660d3f 100644 --- a/include/ejabberd_auth.hrl +++ b/include/ejabberd_auth.hrl @@ -18,5 +18,5 @@ %%% %%%---------------------------------------------------------------------- --record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', +-record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | {binary(), binary(), atom()} | '$1', password = <<"">> :: binary() | scram() | '_'}). diff --git a/rebar.config b/rebar.config index 81cc3ec7f..f47955841 100644 --- a/rebar.config +++ b/rebar.config @@ -77,7 +77,7 @@ {stringprep, "~> 1.0.29", {git, "https://github.com/processone/stringprep", {tag, "1.0.30"}}}, {if_var_true, stun, {stun, "~> 1.2.12", {git, "https://github.com/processone/stun", {tag, "1.2.15"}}}}, - {xmpp, "~> 1.9.2", {git, "https://github.com/processone/xmpp", "be24923968261c2661a9e116650dce5ac95c9d23"}}, + {xmpp, "~> 1.9.2", {git, "https://github.com/processone/xmpp", "8929be60aa0c56653b13f5fcada1e460337a016b"}}, {yconf, "~> 1.0.17", {git, "https://github.com/processone/yconf", "9682a6025ed543eedf34637e4cfcc66837074af6"}} ]}. diff --git a/rebar.lock b/rebar.lock index 01d179d78..68e7d5ae7 100644 --- a/rebar.lock +++ b/rebar.lock @@ -26,7 +26,7 @@ {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1}, {<<"xmpp">>, {git,"https://github.com/processone/xmpp", - {ref,"be24923968261c2661a9e116650dce5ac95c9d23"}}, + {ref,"8929be60aa0c56653b13f5fcada1e460337a016b"}}, 0}, {<<"yconf">>, {git,"https://github.com/processone/yconf", diff --git a/sql/lite.new.sql b/sql/lite.new.sql index 7248d080b..42f289fb3 100644 --- a/sql/lite.new.sql +++ b/sql/lite.new.sql @@ -18,13 +18,14 @@ CREATE TABLE users ( username text NOT NULL, + type smallint, server_host text NOT NULL, password text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (server_host, username) + PRIMARY KEY (server_host, username, type) ); diff --git a/sql/lite.sql b/sql/lite.sql index 2958a9628..b31e02b79 100644 --- a/sql/lite.sql +++ b/sql/lite.sql @@ -17,12 +17,14 @@ -- CREATE TABLE users ( - username text PRIMARY KEY, + username text, + type smallint, password text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + primary key (username, type) ); diff --git a/sql/mssql.new.sql b/sql/mssql.new.sql index 3e6817012..f67033eed 100644 --- a/sql/mssql.new.sql +++ b/sql/mssql.new.sql @@ -403,6 +403,7 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW CREATE TABLE [dbo].[users] ( [username] [varchar] (250) NOT NULL, [server_host] [varchar] (250) NOT NULL, + [type] [smallint] NOT NULL, [password] [text] NOT NULL, [serverkey] [text] NOT NULL DEFAULT '', [salt] [text] NOT NULL DEFAULT '', @@ -411,7 +412,8 @@ CREATE TABLE [dbo].[users] ( CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED ( [server_host] ASC, - [username] ASC + [username] ASC, + [type] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; diff --git a/sql/mssql.sql b/sql/mssql.sql index 5ee11d880..ab5596d48 100644 --- a/sql/mssql.sql +++ b/sql/mssql.sql @@ -379,6 +379,7 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW CREATE TABLE [dbo].[users] ( [username] [varchar] (250) NOT NULL, + [type] [smallint] NOT NULL, [password] [text] NOT NULL, [serverkey] [text] NOT NULL DEFAULT '', [salt] [text] NOT NULL DEFAULT '', @@ -386,7 +387,8 @@ CREATE TABLE [dbo].[users] ( [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED ( - [username] ASC + [username] ASC, + [type] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; diff --git a/sql/mysql.new.sql b/sql/mysql.new.sql index c5a4675b0..cf818ad3d 100644 --- a/sql/mysql.new.sql +++ b/sql/mysql.new.sql @@ -18,13 +18,14 @@ CREATE TABLE users ( username varchar(191) NOT NULL, + type smallint NOT NULL, server_host varchar(191) NOT NULL, password text NOT NULL, serverkey varchar(128) NOT NULL DEFAULT '', salt varchar(128) NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (server_host(191), username) + PRIMARY KEY (server_host(191), username, type) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Add support for SCRAM auth to a database created before ejabberd 16.03: diff --git a/sql/mysql.sql b/sql/mysql.sql index 479ccb435..210f41713 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -17,12 +17,14 @@ -- CREATE TABLE users ( - username varchar(191) PRIMARY KEY, + username varchar(191) NOT NULL, + type smallint NOT NULL,, password text NOT NULL, serverkey varchar(128) NOT NULL DEFAULT '', salt varchar(128) NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (username, type) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Add support for SCRAM auth to a database created before ejabberd 16.03: diff --git a/sql/pg.new.sql b/sql/pg.new.sql index a7e79fee3..5bd107d97 100644 --- a/sql/pg.new.sql +++ b/sql/pg.new.sql @@ -172,12 +172,13 @@ CREATE TABLE users ( username text NOT NULL, server_host text NOT NULL, + "type" smallint NOT NULL, "password" text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT now(), - PRIMARY KEY (server_host, username) + PRIMARY KEY (server_host, username, "type") ); -- Add support for SCRAM auth to a database created before ejabberd 16.03: diff --git a/sql/pg.sql b/sql/pg.sql index 2cc8d4c95..75674e3a8 100644 --- a/sql/pg.sql +++ b/sql/pg.sql @@ -17,12 +17,14 @@ -- CREATE TABLE users ( - username text PRIMARY KEY, + username text NOT NULL, + "type" smallint NOT NULL "password" text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT now() + created_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (username, "type") ); -- Add support for SCRAM auth to a database created before ejabberd 16.03: diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 4ead00f48..aadbcf631 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -48,7 +48,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([auth_modules/1, convert_to_scram/1]). +-export([auth_modules/1, convert_to_scram/1, drop_password_type/2, set_password_instance/3]). -include_lib("xmpp/include/scram.hrl"). -include("logger.hrl"). @@ -76,26 +76,38 @@ -callback store_type(binary()) -> plain | external | scram. -callback set_password(binary(), binary(), password()) -> {ets_cache:tag(), {ok, password()} | {error, db_failure | not_allowed}}. +-callback set_password_multiple(binary(), binary(), [password()]) -> + {ets_cache:tag(), {ok, [password()]} | {error, db_failure | not_allowed}}. +-callback set_password_instance(binary(), binary(), password()) -> + ok | {error, db_failure | not_allowed}. -callback remove_user(binary(), binary()) -> ok | {error, db_failure | not_allowed}. -callback user_exists(binary(), binary()) -> {ets_cache:tag(), boolean() | {error, db_failure}}. -callback check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}. -callback try_register(binary(), binary(), password()) -> {ets_cache:tag(), {ok, password()} | {error, exists | db_failure | not_allowed}}. +-callback try_register_multiple(binary(), binary(), [password()]) -> + {ets_cache:tag(), {ok, [password()]} | {error, exists | db_failure | not_allowed}}. -callback get_users(binary(), opts()) -> [{binary(), binary()}]. -callback count_users(binary(), opts()) -> number(). --callback get_password(binary(), binary()) -> {ets_cache:tag(), {ok, password()} | error}. +-callback get_password(binary(), binary()) -> {ets_cache:tag(), {ok, password() | [password()]} | error}. +-callback drop_password_type(binary(), atom()) -> + ok | {error, db_failure | not_allowed}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> boolean(). -optional_callbacks([reload/1, set_password/3, + set_password_multiple/3, + set_password_instance/3, remove_user/2, user_exists/2, check_password/4, try_register/3, + try_register_multiple/3, get_users/2, count_users/2, get_password/2, + drop_password_type/2, use_cache/1, cache_nodes/1]). @@ -265,15 +277,41 @@ check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGe false end. +convert_password_for_storage(_Server, #scram{} = Password) -> + {Password, [Password]}; +convert_password_for_storage(Server, Password) -> + P = case ejabberd_option:auth_stored_password_types(Server) of + [] -> + case ejabberd_option:auth_password_format(Server) of + plain -> + [Password]; + _ -> + [password_to_scram(Server, Password)] + end; + M -> + lists:sort(lists:map( + fun(scram_sha1) -> + password_to_scram(Server, Password, sha, ?SCRAM_DEFAULT_ITERATION_COUNT); + (scram_sha256) -> + password_to_scram(Server, Password, sha256, ?SCRAM_DEFAULT_ITERATION_COUNT); + (scram_sha512) -> + password_to_scram(Server, Password, sha512, ?SCRAM_DEFAULT_ITERATION_COUNT); + (plain) -> + Password + end, M)) + end, + {Password, P}. + -spec set_password(binary(), binary(), password()) -> ok | {error, db_failure | not_allowed | invalid_jid | invalid_password}. set_password(User, Server, Password) -> case validate_credentials(User, Server, Password) of {ok, LUser, LServer} -> + {Plain, Passwords} = convert_password_for_storage(Server, Password), lists:foldl( fun(M, {error, _}) -> - db_set_password(LUser, LServer, Password, M); + db_set_password(LUser, LServer, Plain, Passwords, M); (_, ok) -> ok end, {error, not_allowed}, auth_modules(LServer)); @@ -281,6 +319,32 @@ set_password(User, Server, Password) -> Err end. +set_password_instance(User, Server, Password) -> + case validate_credentials(User, Server, Password) of + {ok, LUser, LServer} -> + lists:foldl( + fun(Mod, {error, _} = Acc) -> + case erlang:function_exported(Mod, set_password_instance, 3) of + true -> + R = Mod:set_password_instance(LUser, LServer, Password), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(cache_tab(Mod), {LUser, LServer}, + cache_nodes(Mod, LServer)); + _ -> + ok + end, + R; + _ -> + Acc + end; + (_, ok) -> + ok + end, {error, not_allowed}, auth_modules(LServer)); + Err -> + Err + end. + -spec try_register(binary(), binary(), password()) -> ok | {error, db_failure | not_allowed | exists | invalid_jid | invalid_password}. @@ -293,22 +357,23 @@ try_register(User, Server, Password) -> false -> case ejabberd_router:is_my_host(LServer) of true -> - case ejabberd_hooks:run_fold(check_register_user, LServer, true, [User, Server, Password]) of + case ejabberd_hooks:run_fold(check_register_user, LServer, true, + [User, Server, Password]) of true -> - case lists:foldl( - fun(_, ok) -> - ok; - (Mod, _) -> - db_try_register( - LUser, LServer, Password, Mod) - end, {error, not_allowed}, - auth_modules(LServer)) of - ok -> - ejabberd_hooks:run( - register_user, LServer, [LUser, LServer]); - {error, _} = Err -> - Err - end; + {Plain, Passwords} = convert_password_for_storage(Server, Password), + case lists:foldl( + fun(_, ok) -> + ok; + (Mod, _) -> + db_try_register( + LUser, LServer, Plain, Passwords, Mod) + end, {error, not_allowed}, auth_modules(LServer)) of + ok -> + ejabberd_hooks:run( + register_user, LServer, [LUser, LServer]); + {error, _} = Err -> + Err + end; false -> {error, not_allowed} end; @@ -356,32 +421,27 @@ count_users(Server, Opts) -> auth_modules(LServer))) end. --spec get_password(binary(), binary()) -> false | password(). +-spec get_password(binary(), binary()) -> false | [password()]. get_password(User, Server) -> - case validate_credentials(User, Server) of - {ok, LUser, LServer} -> - case lists:foldl( - fun(M, error) -> db_get_password(LUser, LServer, M); - (_M, Acc) -> Acc - end, error, auth_modules(LServer)) of - {ok, Password} -> - Password; - error -> - false - end; - _ -> - false + case get_password_with_authmodule(User, Server) of + {Passwords, _} -> Passwords end. -spec get_password_s(binary(), binary()) -> password(). get_password_s(User, Server) -> case get_password(User, Server) of false -> <<"">>; - Password -> Password + Passwords -> + {_, Pass} = lists:foldl( + fun(Plain, _) when is_binary(Plain) -> {true, Plain}; + (Pass, {false, _}) -> {true, Pass}; + (_, Acc) -> Acc + end, {false, <<"">>}, Passwords), + Pass end. -spec get_password_with_authmodule(binary(), binary()) -> - {false | {false, atom(), binary()} | password(), module()}. + {false | {false, atom(), binary()} | [password()], module()}. get_password_with_authmodule(User, Server) -> case validate_credentials(User, Server) of {ok, LUser, LServer} -> @@ -395,8 +455,10 @@ get_password_with_authmodule(User, Server) -> (_M, Acc) -> Acc end, {error, undefined}, auth_modules(LServer)) of - {{ok, Password}, Module} -> + {{ok, Password}, Module} when is_list(Password) -> {Password, Module}; + {{ok, Password}, Module} -> + {[Password], Module}; {error, Module} -> {false, Module} end @@ -455,6 +517,30 @@ user_exists_in_other_modules_loop([AuthModule | AuthModules], User, Server) -> maybe_exists end. +drop_password_type(LServer, Type) -> + Hash = case Type of + plain -> plain; + scram_sha1 -> sha; + scram_sha256 -> sha256; + scram_sha512 -> sha512 + end, + lists:foreach( + fun(M) -> + case erlang:function_exported(M, drop_password_type, 2) of + true -> + M:drop_password_type(LServer, Hash), + case use_cache(M, LServer) of + true -> + ets_cache:clear(cache_tab(M), + cache_nodes(M, LServer)); + false -> + ok + end; + _ -> + ok + end + end, auth_modules(LServer)). + -spec which_users_exists(list({binary(), binary()})) -> list({binary(), binary()}). which_users_exists(USPairs) -> ByServer = lists:foldl( @@ -592,54 +678,86 @@ get_is_banned(User, Server) -> %%%---------------------------------------------------------------------- %%% Backend calls %%%---------------------------------------------------------------------- --spec db_try_register(binary(), binary(), password(), module()) -> ok | {error, exists | db_failure | not_allowed}. -db_try_register(User, Server, Password, Mod) -> - case erlang:function_exported(Mod, try_register, 3) of +-spec db_try_register(binary(), binary(), binary(), [password()], module()) -> ok | {error, exists | db_failure | not_allowed}. +db_try_register(User, Server, PlainPassword, Passwords, Mod) -> + Ret = case erlang:function_exported(Mod, try_register_multiple, 3) of true -> - Password1 = case Mod:store_type(Server) of - scram -> password_to_scram(Server, Password); - _ -> Password - end, - Ret = case use_cache(Mod, Server) of - true -> - ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, Password}, - fun() -> Mod:try_register(User, Server, Password1) end, - cache_nodes(Mod, Server)); - false -> - ets_cache:untag(Mod:try_register(User, Server, Password1)) - end, - case Ret of - {ok, _} -> ok; - {error, _} = Err -> Err + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, Passwords}, + fun() -> Mod:try_register_multiple(User, Server, Passwords) end, + cache_nodes(Mod, Server)); + false -> + ets_cache:untag(Mod:try_register_multiple(User, Server, Passwords)) end; - false -> - {error, not_allowed} + _ -> + case erlang:function_exported(Mod, try_register, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, [PlainPassword]}, + fun() -> + case Mod:try_register(User, Server, PlainPassword) of + {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; + Other -> Other + end + end, cache_nodes(Mod, Server)); + false -> + case Mod:try_register(User, Server, PlainPassword) of + {_, {ok, Pass}} -> {ok, [Pass]}; + V -> ets_cache:untag(V) + end + end; + false -> + {error, not_allowed} + end + end, + case Ret of + {ok, _} -> ok; + {error, _} = Err -> Err end. --spec db_set_password(binary(), binary(), password(), module()) -> ok | {error, db_failure | not_allowed}. -db_set_password(User, Server, Password, Mod) -> - case erlang:function_exported(Mod, set_password, 3) of - true -> - Password1 = case Mod:store_type(Server) of - scram -> password_to_scram(Server, Password); - _ -> Password - end, - Ret = case use_cache(Mod, Server) of +-spec db_set_password(binary(), binary(), binary(), [password()], module()) -> ok | {error, db_failure | not_allowed}. +db_set_password(User, Server, PlainPassword, Passwords, Mod) -> + Ret = case erlang:function_exported(Mod, set_password_multiple, 3) of + true -> + case use_cache(Mod, Server) of true -> ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, Password}, - fun() -> Mod:set_password(User, Server, Password1) end, - cache_nodes(Mod, Server)); + cache_tab(Mod), {User, Server}, {ok, Passwords}, + fun() -> Mod:set_password_multiple(User, Server, Passwords) end, + cache_nodes(Mod, Server)); false -> - ets_cache:untag(Mod:set_password(User, Server, Password1)) - end, - case Ret of - {ok, _} -> ok; - {error, _} = Err -> Err - end; - false -> - {error, not_allowed} + ets_cache:untag(Mod:set_password_multiple(User, Server, Passwords)) + end; + _ -> + case erlang:function_exported(Mod, set_password, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), {User, Server}, {ok, [PlainPassword]}, + fun() -> + case Mod:set_password(User, Server, PlainPassword) of + {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; + Other -> Other + end + end, cache_nodes(Mod, Server)); + false -> + case Mod:set_password(User, Server, PlainPassword) of + {_, {ok, Pass}} -> {ok, [Pass]}; + V -> ets_cache:untag(V) + end + end; + false -> + {error, not_allowed} + end + end, + case Ret of + {ok, _} -> ok; + {error, _} = Err -> Err end. db_get_password(User, Server, Mod) -> @@ -655,10 +773,20 @@ db_get_password(User, Server, Mod) -> error; true when UseCache -> ets_cache:lookup( - cache_tab(Mod), {User, Server}, - fun() -> Mod:get_password(User, Server) end); + cache_tab(Mod), {User, Server}, + fun() -> + case Mod:get_password(User, Server) of + {_, {ok, List}} = V when is_list(List) -> V; + {Tag, {ok, Single}} -> {Tag, {ok, [Single]}}; + Other -> Other + end + end); true -> - ets_cache:untag(Mod:get_password(User, Server)) + case Mod:get_password(User, Server) of + {_, {ok, List}} when is_list(List) -> {ok, List}; + {_, {ok, Single}} -> {ok, [Single]}; + Other -> ets_cache:untag(Other) + end end. db_user_exists(User, Server, Mod) -> @@ -703,8 +831,8 @@ db_user_exists(User, Server, Mod) -> db_check_password(User, AuthzId, Server, ProvidedPassword, Digest, DigestFun, Mod) -> case db_get_password(User, Server, Mod) of - {ok, ValidPassword} -> - match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun); + {ok, ValidPasswords} -> + match_passwords(ProvidedPassword, ValidPasswords, Digest, DigestFun); error -> case {Mod:store_type(Server), use_cache(Mod, Server)} of {external, true} -> @@ -809,7 +937,9 @@ password_to_scram(Host, Password) -> password_to_scram(_Host, #scram{} = Password, _IterationCount) -> Password; password_to_scram(Host, Password, IterationCount) -> - Hash = ejabberd_option:auth_scram_hash(Host), + password_to_scram(Host, Password, ejabberd_option:auth_scram_hash(Host), IterationCount). + +password_to_scram(_Host, Password, Hash, IterationCount) -> Salt = p1_rand:bytes(?SALT_LENGTH), SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), @@ -897,11 +1027,19 @@ auth_modules(Server) -> misc:atom_to_binary(M)]) || M <- Methods]. --spec match_passwords(password(), password(), +-spec match_passwords(password(), [password()], binary(), digest_fun() | undefined) -> boolean(). -match_passwords(Password, #scram{} = Scram, <<"">>, undefined) -> +match_passwords(Provided, Passwords, Digest, DigestFun) -> + lists:any( + fun(Pass) -> + match_password(Provided, Pass, Digest, DigestFun) + end, Passwords). + +-spec match_password(password(), password(), + binary(), digest_fun() | undefined) -> boolean(). +match_password(Password, #scram{} = Scram, <<"">>, undefined) -> is_password_scram_valid(Password, Scram); -match_passwords(Password, #scram{} = Scram, Digest, DigestFun) -> +match_password(Password, #scram{} = Scram, Digest, DigestFun) -> StoredKey = base64:decode(Scram#scram.storedkey), DigRes = if Digest /= <<"">> -> Digest == DigestFun(StoredKey); @@ -912,9 +1050,9 @@ match_passwords(Password, #scram{} = Scram, Digest, DigestFun) -> true -> StoredKey == Password andalso Password /= <<"">> end; -match_passwords(ProvidedPassword, ValidPassword, <<"">>, undefined) -> +match_password(ProvidedPassword, ValidPassword, <<"">>, undefined) -> ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>; -match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun) -> +match_password(ProvidedPassword, ValidPassword, Digest, DigestFun) -> DigRes = if Digest /= <<"">> -> Digest == DigestFun(ValidPassword); true -> false @@ -983,7 +1121,7 @@ convert_to_scram(Server) -> lists:foreach( fun({U, S}) -> case get_password(U, S) of - Pass when is_binary(Pass) -> + [Pass] when is_binary(Pass) -> SPass = password_to_scram(Server, Pass), set_password(U, S, SPass); _ -> diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl index 6ed303fae..1fe57aca1 100644 --- a/src/ejabberd_auth_mnesia.erl +++ b/src/ejabberd_auth_mnesia.erl @@ -29,11 +29,11 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, set_password/3, try_register/3, +-export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3, get_users/2, init_db/0, count_users/2, get_password/2, remove_user/2, store_type/1, import/2, - plain_password_required/1, use_cache/1]). + plain_password_required/1, use_cache/1, drop_password_type/2, set_password_instance/3]). -export([need_transform/1, transform/1]). -include("logger.hrl"). @@ -86,30 +86,58 @@ plain_password_required(Server) -> store_type(Server) -> ejabberd_auth:password_format(Server). -set_password(User, Server, Password) -> - US = {User, Server}, - F = fun () -> - mnesia:write(#passwd{us = US, password = Password}) +set_password_multiple(User, Server, Passwords) -> + F = fun() -> + lists:foreach( + fun(#scram{hash = Hash} = Password) -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + (Plain) -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end, Passwords) end, case mnesia:transaction(F) of {atomic, ok} -> - {cache, {ok, Password}}; + {cache, {ok, Passwords}}; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {nocache, {error, db_failure}} end. -try_register(User, Server, Password) -> - US = {User, Server}, - F = fun () -> - case mnesia:read({passwd, US}) of - [] -> - mnesia:write(#passwd{us = US, password = Password}), - mnesia:dirty_update_counter(reg_users_counter, Server, 1), - {ok, Password}; - [_] -> - {error, exists} - end +set_password_instance(User, Server, Password) -> + F = fun() -> + case Password of + #scram{hash = Hash} = Password -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + Plain -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. + +try_register_multiple(User, Server, Passwords) -> + F = fun() -> + case mnesia:select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'}, + [{'==', '$1', User}, + {'==', '$2', Server}], + ['$3']}]) of + [] -> + lists:foreach( + fun(#scram{hash = Hash} = Password) -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + (Plain) -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end, Passwords), + mnesia:dirty_update_counter(reg_users_counter, Server, 1), + {ok, Passwords}; + [_] -> + {error, exists} + end end, case mnesia:transaction(F) of {atomic, Res} -> @@ -120,9 +148,10 @@ try_register(User, Server, Password) -> end. get_users(Server, []) -> - mnesia:dirty_select(passwd, + Users = mnesia:dirty_select(passwd, [{#passwd{us = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, Server}], ['$1']}]); + [{'==', {element, 2, '$1'}, Server}], ['$1']}]), + lists:uniq(lists:map(fun({U, S, _}) -> {U, S} end, Users)); get_users(Server, [{from, Start}, {to, End}]) when is_integer(Start) and is_integer(End) -> get_users(Server, [{limit, End - Start + 1}, {offset, Start}]); @@ -179,22 +208,48 @@ count_users(Server, _) -> count_users(Server, []). get_password(User, Server) -> - case mnesia:dirty_read(passwd, {User, Server}) of - [{passwd, _, {scram, SK, SEK, Salt, IC}}] -> - {cache, {ok, #scram{storedkey = SK, serverkey = SEK, - salt = Salt, hash = sha, iterationcount = IC}}}; - [#passwd{password = Password}] -> - {cache, {ok, Password}}; + case mnesia:dirty_select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'}, + [{'==', '$1', User}, + {'==', '$2', Server}], + ['$3']}]) of + [_|_] = List -> + List2 = lists:map( + fun({scram, SK, SEK, Salt, IC}) -> + #scram{storedkey = SK, serverkey = SEK, + salt = Salt, hash = sha, iterationcount = IC}; + (Other) -> Other + end, List), + {cache, {ok, List2}}; _ -> {cache, error} end. +drop_password_type(Server, Hash) -> + F = fun() -> + Keys = mnesia:select(passwd, [{{'_', '$1', '_'}, + [{'==', {element, 3, '$1'}, Hash}, + {'==', {element, 2, '$1'}, Server}], + ['$1']}]), + lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), + ok + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} + end. + remove_user(User, Server) -> - US = {User, Server}, F = fun () -> - mnesia:delete({passwd, US}), - mnesia:dirty_update_counter(reg_users_counter, Server, -1), - ok + Keys = mnesia:select(passwd, [{{'_', '$1', '_'}, + [{'==', {element, 1, '$1'}, User}, + {'==', {element, 2, '$1'}, Server}], + ['$1']}]), + lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), + mnesia:dirty_update_counter(reg_users_counter, Server, -1), + ok end, case mnesia:transaction(F) of {atomic, ok} -> @@ -206,45 +261,10 @@ remove_user(User, Server) -> need_transform(#reg_users_counter{}) -> false; -need_transform({passwd, {U, S}, Pass}) -> - case Pass of - _ when is_binary(Pass) -> - case store_type(S) of - scram -> - ?INFO_MSG("Passwords in Mnesia table 'passwd' " - "will be SCRAM'ed", []), - true; - plain -> - false - end; - {scram, _, _, _, _} -> - case store_type(S) of - scram -> - false; - plain -> - ?WARNING_MSG("Some passwords were stored in the database " - "as SCRAM, but 'auth_password_format' " - "is not configured as 'scram': some " - "authentication mechanisms such as DIGEST-MD5 " - "would *fail*", []), - false - end; - #scram{} -> - case store_type(S) of - scram -> - false; - plain -> - ?WARNING_MSG("Some passwords were stored in the database " - "as SCRAM, but 'auth_password_format' " - "is not configured as 'scram': some " - "authentication mechanisms such as DIGEST-MD5 " - "would *fail*", []), - false - end; - _ when is_list(U) orelse is_list(S) orelse is_list(Pass) -> - ?INFO_MSG("Mnesia table 'passwd' will be converted to binary", []), - true - end. +need_transform({passwd, {_U, _S, _T}, _Pass}) -> + false; +need_transform({passwd, {_U, _S}, _Pass}) -> + true. transform({passwd, {U, S}, Pass}) when is_list(U) orelse is_list(S) orelse is_list(Pass) -> @@ -263,24 +283,14 @@ transform({passwd, {U, S}, Pass}) transform(#passwd{us = NewUS, password = NewPass}); transform(#passwd{us = {U, S}, password = Password} = P) when is_binary(Password) -> - case store_type(S) of - scram -> - case jid:resourceprep(Password) of - error -> - ?ERROR_MSG("SASLprep failed for password of user ~ts@~ts", - [U, S]), - P; - _ -> - Scram = ejabberd_auth:password_to_scram(S, Password), - P#passwd{password = Scram} - end; - plain -> - P - end; -transform({passwd, _, {scram, _, _, _, _}} = P) -> - P; -transform(#passwd{password = #scram{}} = P) -> - P. + P#passwd{us = {U, S, plain}, password = Password}; +transform({passwd, {U, S}, {scram, SK, SEK, Salt, IC}}) -> + #passwd{us = {U, S, sha}, + password = #scram{storedkey = SK, serverkey = SEK, + salt = Salt, hash = sha, iterationcount = IC}}; +transform(#passwd{us = {U, S}, password = #scram{hash = Hash}} = P) -> + P#passwd{us = {U, S, Hash}}; +transform(Other) -> Other. import(LServer, [LUser, Password, _TimeStamp]) -> mnesia:dirty_write( diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index fefabdf76..b8ff98615 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -30,10 +30,10 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, set_password/3, try_register/3, +-export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3, get_users/2, count_users/2, get_password/2, remove_user/2, store_type/1, plain_password_required/1, - export/1, which_users_exists/2]). + export/1, which_users_exists/2, drop_password_type/2, set_password_instance/3]). -export([sql_schemas/0]). -include_lib("xmpp/include/scram.hrl"). @@ -49,7 +49,34 @@ start(Host) -> ok. sql_schemas() -> - [#sql_schema{ + [ + #sql_schema{ + version = 2, + tables = + [#sql_table{ + name = <<"users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"type">>, type = smallint}, + #sql_column{name = <<"password">>, type = text}, + #sql_column{name = <<"serverkey">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"salt">>, type = {text, 128}, + default = true}, + #sql_column{name = <<"iterationcount">>, type = integer, + default = true}, + #sql_column{name = <<"created_at">>, type = timestamp, + default = true}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"type">>], + unique = true}]}], + update = [ + {add_column, <<"users">>, <<"type">>}, + {update_primary_key,<<"users">>, + [<<"server_host">>, <<"username">>, <<"type">>]} + ]}, + #sql_schema{ version = 1, tables = [#sql_table{ @@ -78,42 +105,87 @@ plain_password_required(Server) -> store_type(Server) -> ejabberd_auth:password_format(Server). -set_password(User, Server, Password) -> +hash_to_num(plain) -> 1; +hash_to_num(sha) -> 2; +hash_to_num(sha256) -> 3; +hash_to_num(sha512) -> 4. + +num_to_hash(2) -> sha; +num_to_hash(3) -> sha256; +num_to_hash(4) -> sha512. + +set_password_instance(User, Server, #scram{hash = Hash, storedkey = SK, serverkey = SEK, + salt = Salt, iterationcount = IC}) -> + F = fun() -> + set_password_scram_t(User, Server, Hash, + SK, SEK, Salt, IC) + end, + case ejabberd_sql:sql_transaction(Server, F) of + {atomic, _} -> + ok; + {aborted, _} -> + {error, db_failure} + end; +set_password_instance(User, Server, Plain) -> + F = fun() -> + set_password_t(User, Server, Plain) + end, + case ejabberd_sql:sql_transaction(Server, F) of + {atomic, _} -> + ok; + {aborted, _} -> + {error, db_failure} + end. + +set_password_multiple(User, Server, Passwords) -> F = fun() -> - case Password of - #scram{hash = Hash, storedkey = SK, serverkey = SEK, - salt = Salt, iterationcount = IC} -> - SK2 = scram_hash_encode(Hash, SK), + ejabberd_sql:sql_query_t( + ?SQL("delete from users where username=%(User)s and %(Server)H")), + lists:foreach( + fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK, + salt = Salt, iterationcount = IC}) -> set_password_scram_t( - User, Server, - SK2, SEK, Salt, IC); - _ -> - set_password_t(User, Server, Password) - end + User, Server, Hash, + SK, SEK, Salt, IC); + (Plain) -> + set_password_t(User, Server, Plain) + end, Passwords) end, case ejabberd_sql:sql_transaction(Server, F) of {atomic, _} -> - {cache, {ok, Password}}; + {cache, {ok, Passwords}}; {aborted, _} -> {nocache, {error, db_failure}} end. -try_register(User, Server, Password) -> - Res = - case Password of - #scram{hash = Hash, storedkey = SK, serverkey = SEK, - salt = Salt, iterationcount = IC} -> - SK2 = scram_hash_encode(Hash, SK), - add_user_scram( - Server, User, - SK2, SEK, Salt, IC); - _ -> - add_user(Server, User, Password) - end, - case Res of - {updated, 1} -> {cache, {ok, Password}}; - _ -> {nocache, {error, exists}} +try_register_multiple(User, Server, Passwords) -> + F = + fun() -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(count(*))d from users where username=%(User)s and %(Server)H")) of + {selected, [{0}]} -> + lists:foreach( + fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK, + salt = Salt, iterationcount = IC}) -> + set_password_scram_t( + User, Server, Hash, + SK, SEK, Salt, IC); + (Plain) -> + set_password_t(User, Server, Plain) + end, Passwords), + {cache, {ok, Passwords}}; + {selected, _} -> + {nocache, {error, exists}}; + _ -> + {nocache, {error, db_failure}} + end + end, + case ejabberd_sql:sql_transaction(Server, F) of + {atomic, Res} -> + Res; + {aborted, _} -> + {nocache, {error, db_failure}} end. get_users(Server, Opts) -> @@ -132,21 +204,41 @@ count_users(Server, Opts) -> get_password(User, Server) -> case get_password_scram(Server, User) of - {selected, [{Password, <<>>, <<>>, 0}]} -> - {cache, {ok, Password}}; - {selected, [{StoredKey, ServerKey, Salt, IterationCount}]} -> - {Hash, SK} = case StoredKey of - <<"sha256:", Rest/binary>> -> {sha256, Rest}; - <<"sha512:", Rest/binary>> -> {sha512, Rest}; - Other -> {sha, Other} - end, - {cache, {ok, #scram{storedkey = SK, - serverkey = ServerKey, - salt = Salt, - hash = Hash, - iterationcount = IterationCount}}}; {selected, []} -> {cache, error}; + {selected, Passwords} -> + Converted = lists:map( + fun({0, Password, <<>>, <<>>, 0}) -> + update_password_type(User, Server, 1), + Password; + ({_, Password, <<>>, <<>>, 0}) -> + Password; + ({0, StoredKey, ServerKey, Salt, IterationCount}) -> + {Hash, SK} = case StoredKey of + <<"sha256:", Rest/binary>> -> + update_password_type(User, Server, 3, Rest), + {sha256, Rest}; + <<"sha512:", Rest/binary>> -> + update_password_type(User, Server, 4, Rest), + {sha512, Rest}; + Other -> + update_password_type(User, Server, 2), + {sha, Other} + end, + #scram{storedkey = SK, + serverkey = ServerKey, + salt = Salt, + hash = Hash, + iterationcount = IterationCount}; + ({Type, StoredKey, ServerKey, Salt, IterationCount}) -> + Hash = num_to_hash(Type), + #scram{storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt, + hash = Hash, + iterationcount = IterationCount} + end, Passwords), + {cache, {ok, Converted}}; _ -> {nocache, error} end. @@ -159,6 +251,13 @@ remove_user(User, Server) -> {error, db_failure} end. +drop_password_type(LServer, Hash) -> + Type = hash_to_num(Hash), + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from users" + " where type=%(Type)d and %(LServer)H")). + scram_hash_encode(Hash, StoreKey) -> case Hash of sha -> StoreKey; @@ -166,12 +265,14 @@ scram_hash_encode(Hash, StoreKey) -> sha512 -> <<"sha512:", StoreKey/binary>> end. -set_password_scram_t(LUser, LServer, +set_password_scram_t(LUser, LServer, Hash, StoredKey, ServerKey, Salt, IterationCount) -> + Type = hash_to_num(Hash), ?SQL_UPSERT_T( "users", ["!username=%(LUser)s", "!server_host=%(LServer)s", + "!type=%(Type)d", "password=%(StoredKey)s", "serverkey=%(ServerKey)s", "salt=%(Salt)s", @@ -182,40 +283,31 @@ set_password_t(LUser, LServer, Password) -> "users", ["!username=%(LUser)s", "!server_host=%(LServer)s", + "!type=1", "password=%(Password)s", "serverkey=''", "salt=''", "iterationcount=0"]). +update_password_type(LUser, LServer, Type, Password) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("update users set type=%(Type)d, password=%(Password)s" + " where username=%(LUser)s and type=0 and %(LServer)H")). + +update_password_type(LUser, LServer, Type) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("update users set type=%(Type)d" + " where username=%(LUser)s and type=0 and %(LServer)H")). + get_password_scram(LServer, LUser) -> ejabberd_sql:sql_query( LServer, - ?SQL("select @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" + ?SQL("select @(type)d, @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" " from users" " where username=%(LUser)s and %(LServer)H")). -add_user_scram(LServer, LUser, - StoredKey, ServerKey, Salt, IterationCount) -> - ejabberd_sql:sql_query( - LServer, - ?SQL_INSERT( - "users", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "password=%(StoredKey)s", - "serverkey=%(ServerKey)s", - "salt=%(Salt)s", - "iterationcount=%(IterationCount)d"])). - -add_user(LServer, LUser, Password) -> - ejabberd_sql:sql_query( - LServer, - ?SQL_INSERT( - "users", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "password=%(Password)s"])). - del_user(LServer, LUser) -> ejabberd_sql:sql_query( LServer, @@ -224,7 +316,7 @@ del_user(LServer, LUser) -> list_users(LServer, []) -> ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users where %(LServer)H")); + ?SQL("select @(distinct username)s from users where %(LServer)H")); list_users(LServer, [{from, Start}, {to, End}]) when is_integer(Start) and is_integer(End) -> list_users(LServer, @@ -240,7 +332,7 @@ list_users(LServer, [{limit, Limit}, {offset, Offset}]) when is_integer(Limit) and is_integer(Offset) -> ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users " + ?SQL("select @(distinct username)s from users " "where %(LServer)H " "order by username " "limit %(Limit)d offset %(Offset)d")); @@ -252,7 +344,7 @@ list_users(LServer, SPrefix2 = <>, ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users " + ?SQL("select @(distinct username)s from users " "where username like %(SPrefix2)s %ESCAPE and %(LServer)H " "order by username " "limit %(Limit)d offset %(Offset)d")). @@ -269,11 +361,11 @@ users_number(LServer) -> " where oid = 'users'::regclass::oid")); _ -> ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from users where %(LServer)H")) + ?SQL("select @(count(distinct username))d from users where %(LServer)H")) end; (_Type, _) -> ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from users where %(LServer)H")) + ?SQL("select @(count(distinct username))d from users where %(LServer)H")) end). users_number(LServer, [{prefix, Prefix}]) @@ -282,7 +374,7 @@ users_number(LServer, [{prefix, Prefix}]) SPrefix2 = <>, ejabberd_sql:sql_query( LServer, - ?SQL("select @(count(*))d from users " + ?SQL("select @(count(distinct username))d from users " "where username like %(SPrefix2)s %ESCAPE and %(LServer)H")); users_number(LServer, []) -> users_number(LServer). @@ -290,7 +382,7 @@ users_number(LServer, []) -> which_users_exists(LServer, LUsers) when length(LUsers) =< 100 -> try ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users where username in %(LUsers)ls")) of + ?SQL("select @(distinct username)s from users where username in %(LUsers)ls")) of {selected, Matching} -> [U || {U} <- Matching]; {error, _} = E -> diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index f00755625..1852dde32 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -423,16 +423,27 @@ sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = St Type = ejabberd_auth:store_type(LServer), Mechs1 = ejabberd_option:disable_sasl_mechanisms(LServer), - ScramHash = ejabberd_option:auth_scram_hash(LServer), - ShaAv = Type == plain orelse (Type == scram andalso ScramHash == sha), - Sha256Av = Type == plain orelse (Type == scram andalso ScramHash == sha256), - Sha512Av = Type == plain orelse (Type == scram andalso ScramHash == sha512), + {Digest, ShaAv, Sha256Av, Sha512Av} = + case ejabberd_option:auth_stored_password_types(LServer) of + [] -> + ScramHash = ejabberd_option:auth_scram_hash(LServer), + {Type == plain, + Type == plain orelse (Type == scram andalso ScramHash == sha), + Type == plain orelse (Type == scram andalso ScramHash == sha256), + Type == plain orelse (Type == scram andalso ScramHash == sha512)}; + Methods -> + HasPlain = lists:member(plain, Methods), + {HasPlain, + HasPlain orelse lists:member(scram_sha1, Methods), + HasPlain orelse lists:member(scram_sha256, Methods), + HasPlain orelse lists:member(scram_sha512, Methods)} + end, %% I re-created it from cyrsasl ets magic, but I think it's wrong %% TODO: need to check before 18.09 release lists:filter( fun(<<"ANONYMOUS">>) -> ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer); - (<<"DIGEST-MD5">>) -> Type == plain; + (<<"DIGEST-MD5">>) -> Digest; (<<"SCRAM-SHA-1">>) -> ShaAv; (<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted; (<<"SCRAM-SHA-256">>) -> Sha256Av; diff --git a/src/ejabberd_option.erl b/src/ejabberd_option.erl index cb3765b58..2a5c36963 100644 --- a/src/ejabberd_option.erl +++ b/src/ejabberd_option.erl @@ -19,6 +19,7 @@ -export([auth_opts/0, auth_opts/1]). -export([auth_password_format/0, auth_password_format/1]). -export([auth_scram_hash/0, auth_scram_hash/1]). +-export([auth_stored_password_types/0, auth_stored_password_types/1]). -export([auth_use_cache/0, auth_use_cache/1]). -export([c2s_cafile/0, c2s_cafile/1]). -export([c2s_ciphers/0, c2s_ciphers/1]). @@ -264,6 +265,13 @@ auth_scram_hash() -> auth_scram_hash(Host) -> ejabberd_config:get_option({auth_scram_hash, Host}). +-spec auth_stored_password_types() -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512']. +auth_stored_password_types() -> + auth_stored_password_types(global). +-spec auth_stored_password_types(global | binary()) -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512']. +auth_stored_password_types(Host) -> + ejabberd_config:get_option({auth_stored_password_types, Host}). + -spec auth_use_cache() -> boolean(). auth_use_cache() -> auth_use_cache(global). diff --git a/src/ejabberd_options.erl b/src/ejabberd_options.erl index 0e471059e..8dc88cd23 100644 --- a/src/ejabberd_options.erl +++ b/src/ejabberd_options.erl @@ -77,6 +77,8 @@ opt_type(auth_opts) -> {path_prefix, V} end, L) end; +opt_type(auth_stored_password_types) -> + econf:list(econf:enum([plain, scram_sha1, scram_sha256, scram_sha512])); opt_type(auth_password_format) -> econf:enum([plain, scram]); opt_type(auth_scram_hash) -> @@ -546,6 +548,7 @@ options() -> {auth_opts, []}, {auth_password_format, plain}, {auth_scram_hash, sha}, + {auth_stored_password_types, []}, {auth_external_user_exists_check, true}, {auth_use_cache, fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, diff --git a/src/ejabberd_options_doc.erl b/src/ejabberd_options_doc.erl index 312e00f0e..0359b2dbf 100644 --- a/src/ejabberd_options_doc.erl +++ b/src/ejabberd_options_doc.erl @@ -399,6 +399,14 @@ doc() -> "You shouldn't change this if you already have passwords generated with " "a different algorithm - users that have such passwords will not be able " "to authenticate. The default value is 'sha'.")}}, + {auth_stored_password_types, + #{value => "[plain | scram_sha1 | scram_sha256 | scram_sha512]", + desc => + ?T("List with password types that should be stored concurently for each user in database. " + "Each time user set it password, database will be updated to store passwords in format " + "compatible with each format listed here. This can be used to migrate user passwords " + "to more secure format. This options if set, will override values set in 'auth_scream_hash' " + "and 'auth_password_format'. By default this value is not set.")}}, {auth_external_user_exists_check, #{value => "true | false", note => "added in 23.10", diff --git a/src/mod_scram_upgrade.erl b/src/mod_scram_upgrade.erl index e0ffabc70..f08c28631 100644 --- a/src/mod_scram_upgrade.erl +++ b/src/mod_scram_upgrade.erl @@ -107,7 +107,7 @@ c2s_handle_sasl2_task_data({_, #{user := User, server := Server, #scram_upgrade_hash{data = SaltedPassword} -> StoredKey = scram:stored_key(Algo, scram:client_key(Algo, SaltedPassword)), ServerKey = scram:server_key(Algo, SaltedPassword), - ejabberd_auth:set_password(User, Server, + ejabberd_auth:set_password_instance(User, Server, #scram{hash = Algo, iterationcount = Iter, salt = Salt, serverkey = ServerKey, storedkey = StoredKey}), State2 = maps:remove(scram_upgrade, State), diff --git a/test/webadmin_tests.erl b/test/webadmin_tests.erl index 6a4ff7cac..fa12fd6f8 100644 --- a/test/webadmin_tests.erl +++ b/test/webadmin_tests.erl @@ -78,7 +78,7 @@ adduser(Config) -> "server/" ++ binary_to_list(Server) ++ "/users/", <<"register/user=", (mue(User))/binary, "®ister/password=", (mue(Password))/binary, "®ister=Register">>), - Password = ejabberd_auth:get_password(User, Server), + Password = ejabberd_auth:get_password_s(User, Server), ?match({_, _}, binary:match(Body, <<"User ", User/binary, "@", Server/binary, " successfully registered">>)). @@ -92,7 +92,7 @@ changepassword(Config) -> ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", <<"change_password/newpass=", (mue(Password))/binary, "&change_password=Change+Password">>), - ?match(Password, ejabberd_auth:get_password(User, Server)), + ?match(Password, ejabberd_auth:get_password_s(User, Server)), ?match({_, _}, binary:match(Body, <<"
ok
">>)). removeuser(Config) ->