1
0
Fork 0
mirror of https://github.com/processone/ejabberd synced 2025-10-03 17:59:31 +02:00

Add support for storing multiple passwords formats per user

This adds option 'auth_stored_password_types' that can be used to setup
storage of multiple passwords for each user. When this is set, on each
password set, database will now store password in each format specified.
This commit is contained in:
Paweł Chmielowski 2025-03-26 10:27:39 +01:00
parent ced72f4a89
commit 7862c6a7db
20 changed files with 553 additions and 270 deletions

View file

@ -18,5 +18,5 @@
%%% %%%
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
-record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', -record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | {binary(), binary(), atom()} | '$1',
password = <<"">> :: binary() | scram() | '_'}). password = <<"">> :: binary() | scram() | '_'}).

View file

@ -77,7 +77,7 @@
{stringprep, "~> 1.0.29", {git, "https://github.com/processone/stringprep", {tag, "1.0.30"}}}, {stringprep, "~> 1.0.29", {git, "https://github.com/processone/stringprep", {tag, "1.0.30"}}},
{if_var_true, stun, {if_var_true, stun,
{stun, "~> 1.2.12", {git, "https://github.com/processone/stun", {tag, "1.2.15"}}}}, {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"}} {yconf, "~> 1.0.17", {git, "https://github.com/processone/yconf", "9682a6025ed543eedf34637e4cfcc66837074af6"}}
]}. ]}.

View file

@ -26,7 +26,7 @@
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1}, {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1},
{<<"xmpp">>, {<<"xmpp">>,
{git,"https://github.com/processone/xmpp", {git,"https://github.com/processone/xmpp",
{ref,"be24923968261c2661a9e116650dce5ac95c9d23"}}, {ref,"8929be60aa0c56653b13f5fcada1e460337a016b"}},
0}, 0},
{<<"yconf">>, {<<"yconf">>,
{git,"https://github.com/processone/yconf", {git,"https://github.com/processone/yconf",

View file

@ -18,13 +18,14 @@
CREATE TABLE users ( CREATE TABLE users (
username text NOT NULL, username text NOT NULL,
type smallint,
server_host text NOT NULL, server_host text NOT NULL,
password text NOT NULL, password text NOT NULL,
serverkey text NOT NULL DEFAULT '', serverkey text NOT NULL DEFAULT '',
salt text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '',
iterationcount integer NOT NULL DEFAULT 0, iterationcount integer NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (server_host, username) PRIMARY KEY (server_host, username, type)
); );

View file

@ -17,12 +17,14 @@
-- --
CREATE TABLE users ( CREATE TABLE users (
username text PRIMARY KEY, username text,
type smallint,
password text NOT NULL, password text NOT NULL,
serverkey text NOT NULL DEFAULT '', serverkey text NOT NULL DEFAULT '',
salt text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '',
iterationcount integer NOT NULL DEFAULT 0, 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)
); );

View file

@ -403,6 +403,7 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW
CREATE TABLE [dbo].[users] ( CREATE TABLE [dbo].[users] (
[username] [varchar] (250) NOT NULL, [username] [varchar] (250) NOT NULL,
[server_host] [varchar] (250) NOT NULL, [server_host] [varchar] (250) NOT NULL,
[type] [smallint] NOT NULL,
[password] [text] NOT NULL, [password] [text] NOT NULL,
[serverkey] [text] NOT NULL DEFAULT '', [serverkey] [text] NOT NULL DEFAULT '',
[salt] [text] NOT NULL DEFAULT '', [salt] [text] NOT NULL DEFAULT '',
@ -411,7 +412,8 @@ CREATE TABLE [dbo].[users] (
CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED
( (
[server_host] ASC, [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) )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
) TEXTIMAGE_ON [PRIMARY]; ) TEXTIMAGE_ON [PRIMARY];

View file

@ -379,6 +379,7 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW
CREATE TABLE [dbo].[users] ( CREATE TABLE [dbo].[users] (
[username] [varchar] (250) NOT NULL, [username] [varchar] (250) NOT NULL,
[type] [smallint] NOT NULL,
[password] [text] NOT NULL, [password] [text] NOT NULL,
[serverkey] [text] NOT NULL DEFAULT '', [serverkey] [text] NOT NULL DEFAULT '',
[salt] [text] NOT NULL DEFAULT '', [salt] [text] NOT NULL DEFAULT '',
@ -386,7 +387,8 @@ CREATE TABLE [dbo].[users] (
[created_at] [datetime] NOT NULL DEFAULT GETDATE(), [created_at] [datetime] NOT NULL DEFAULT GETDATE(),
CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED 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) )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
) TEXTIMAGE_ON [PRIMARY]; ) TEXTIMAGE_ON [PRIMARY];

View file

@ -18,13 +18,14 @@
CREATE TABLE users ( CREATE TABLE users (
username varchar(191) NOT NULL, username varchar(191) NOT NULL,
type smallint NOT NULL,
server_host varchar(191) NOT NULL, server_host varchar(191) NOT NULL,
password text NOT NULL, password text NOT NULL,
serverkey varchar(128) NOT NULL DEFAULT '', serverkey varchar(128) NOT NULL DEFAULT '',
salt varchar(128) NOT NULL DEFAULT '', salt varchar(128) NOT NULL DEFAULT '',
iterationcount integer NOT NULL DEFAULT 0, iterationcount integer NOT NULL DEFAULT 0,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 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; ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Add support for SCRAM auth to a database created before ejabberd 16.03: -- Add support for SCRAM auth to a database created before ejabberd 16.03:

View file

@ -17,12 +17,14 @@
-- --
CREATE TABLE users ( CREATE TABLE users (
username varchar(191) PRIMARY KEY, username varchar(191) NOT NULL,
type smallint NOT NULL,,
password text NOT NULL, password text NOT NULL,
serverkey varchar(128) NOT NULL DEFAULT '', serverkey varchar(128) NOT NULL DEFAULT '',
salt varchar(128) NOT NULL DEFAULT '', salt varchar(128) NOT NULL DEFAULT '',
iterationcount integer NOT NULL DEFAULT 0, 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; ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Add support for SCRAM auth to a database created before ejabberd 16.03: -- Add support for SCRAM auth to a database created before ejabberd 16.03:

View file

@ -172,12 +172,13 @@
CREATE TABLE users ( CREATE TABLE users (
username text NOT NULL, username text NOT NULL,
server_host text NOT NULL, server_host text NOT NULL,
"type" smallint NOT NULL,
"password" text NOT NULL, "password" text NOT NULL,
serverkey text NOT NULL DEFAULT '', serverkey text NOT NULL DEFAULT '',
salt text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '',
iterationcount integer NOT NULL DEFAULT 0, iterationcount integer NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT now(), 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: -- Add support for SCRAM auth to a database created before ejabberd 16.03:

View file

@ -17,12 +17,14 @@
-- --
CREATE TABLE users ( CREATE TABLE users (
username text PRIMARY KEY, username text NOT NULL,
"type" smallint NOT NULL
"password" text NOT NULL, "password" text NOT NULL,
serverkey text NOT NULL DEFAULT '', serverkey text NOT NULL DEFAULT '',
salt text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '',
iterationcount integer NOT NULL DEFAULT 0, 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: -- Add support for SCRAM auth to a database created before ejabberd 16.03:

View file

@ -48,7 +48,7 @@
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]). 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_lib("xmpp/include/scram.hrl").
-include("logger.hrl"). -include("logger.hrl").
@ -76,26 +76,38 @@
-callback store_type(binary()) -> plain | external | scram. -callback store_type(binary()) -> plain | external | scram.
-callback set_password(binary(), binary(), password()) -> -callback set_password(binary(), binary(), password()) ->
{ets_cache:tag(), {ok, password()} | {error, db_failure | not_allowed}}. {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 remove_user(binary(), binary()) -> ok | {error, db_failure | not_allowed}.
-callback user_exists(binary(), binary()) -> {ets_cache:tag(), boolean() | {error, db_failure}}. -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 check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}.
-callback try_register(binary(), binary(), password()) -> -callback try_register(binary(), binary(), password()) ->
{ets_cache:tag(), {ok, password()} | {error, exists | db_failure | not_allowed}}. {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 get_users(binary(), opts()) -> [{binary(), binary()}].
-callback count_users(binary(), opts()) -> number(). -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 use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> boolean(). -callback cache_nodes(binary()) -> boolean().
-optional_callbacks([reload/1, -optional_callbacks([reload/1,
set_password/3, set_password/3,
set_password_multiple/3,
set_password_instance/3,
remove_user/2, remove_user/2,
user_exists/2, user_exists/2,
check_password/4, check_password/4,
try_register/3, try_register/3,
try_register_multiple/3,
get_users/2, get_users/2,
count_users/2, count_users/2,
get_password/2, get_password/2,
drop_password_type/2,
use_cache/1, use_cache/1,
cache_nodes/1]). cache_nodes/1]).
@ -265,15 +277,41 @@ check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGe
false false
end. 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, -spec set_password(binary(), binary(), password()) -> ok | {error,
db_failure | not_allowed | db_failure | not_allowed |
invalid_jid | invalid_password}. invalid_jid | invalid_password}.
set_password(User, Server, Password) -> set_password(User, Server, Password) ->
case validate_credentials(User, Server, Password) of case validate_credentials(User, Server, Password) of
{ok, LUser, LServer} -> {ok, LUser, LServer} ->
{Plain, Passwords} = convert_password_for_storage(Server, Password),
lists:foldl( lists:foldl(
fun(M, {error, _}) -> fun(M, {error, _}) ->
db_set_password(LUser, LServer, Password, M); db_set_password(LUser, LServer, Plain, Passwords, M);
(_, ok) -> (_, ok) ->
ok ok
end, {error, not_allowed}, auth_modules(LServer)); end, {error, not_allowed}, auth_modules(LServer));
@ -281,6 +319,32 @@ set_password(User, Server, Password) ->
Err Err
end. 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, -spec try_register(binary(), binary(), password()) -> ok | {error,
db_failure | not_allowed | exists | db_failure | not_allowed | exists |
invalid_jid | invalid_password}. invalid_jid | invalid_password}.
@ -293,22 +357,23 @@ try_register(User, Server, Password) ->
false -> false ->
case ejabberd_router:is_my_host(LServer) of case ejabberd_router:is_my_host(LServer) of
true -> 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 -> true ->
case lists:foldl( {Plain, Passwords} = convert_password_for_storage(Server, Password),
fun(_, ok) -> case lists:foldl(
ok; fun(_, ok) ->
(Mod, _) -> ok;
db_try_register( (Mod, _) ->
LUser, LServer, Password, Mod) db_try_register(
end, {error, not_allowed}, LUser, LServer, Plain, Passwords, Mod)
auth_modules(LServer)) of end, {error, not_allowed}, auth_modules(LServer)) of
ok -> ok ->
ejabberd_hooks:run( ejabberd_hooks:run(
register_user, LServer, [LUser, LServer]); register_user, LServer, [LUser, LServer]);
{error, _} = Err -> {error, _} = Err ->
Err Err
end; end;
false -> false ->
{error, not_allowed} {error, not_allowed}
end; end;
@ -356,32 +421,27 @@ count_users(Server, Opts) ->
auth_modules(LServer))) auth_modules(LServer)))
end. end.
-spec get_password(binary(), binary()) -> false | password(). -spec get_password(binary(), binary()) -> false | [password()].
get_password(User, Server) -> get_password(User, Server) ->
case validate_credentials(User, Server) of case get_password_with_authmodule(User, Server) of
{ok, LUser, LServer} -> {Passwords, _} -> Passwords
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
end. end.
-spec get_password_s(binary(), binary()) -> password(). -spec get_password_s(binary(), binary()) -> password().
get_password_s(User, Server) -> get_password_s(User, Server) ->
case get_password(User, Server) of case get_password(User, Server) of
false -> <<"">>; 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. end.
-spec get_password_with_authmodule(binary(), binary()) -> -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) -> get_password_with_authmodule(User, Server) ->
case validate_credentials(User, Server) of case validate_credentials(User, Server) of
{ok, LUser, LServer} -> {ok, LUser, LServer} ->
@ -395,8 +455,10 @@ get_password_with_authmodule(User, Server) ->
(_M, Acc) -> (_M, Acc) ->
Acc Acc
end, {error, undefined}, auth_modules(LServer)) of end, {error, undefined}, auth_modules(LServer)) of
{{ok, Password}, Module} -> {{ok, Password}, Module} when is_list(Password) ->
{Password, Module}; {Password, Module};
{{ok, Password}, Module} ->
{[Password], Module};
{error, Module} -> {error, Module} ->
{false, Module} {false, Module}
end end
@ -455,6 +517,30 @@ user_exists_in_other_modules_loop([AuthModule | AuthModules], User, Server) ->
maybe_exists maybe_exists
end. 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()}). -spec which_users_exists(list({binary(), binary()})) -> list({binary(), binary()}).
which_users_exists(USPairs) -> which_users_exists(USPairs) ->
ByServer = lists:foldl( ByServer = lists:foldl(
@ -592,54 +678,86 @@ get_is_banned(User, Server) ->
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
%%% Backend calls %%% Backend calls
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
-spec db_try_register(binary(), binary(), password(), module()) -> ok | {error, exists | db_failure | not_allowed}. -spec db_try_register(binary(), binary(), binary(), [password()], module()) -> ok | {error, exists | db_failure | not_allowed}.
db_try_register(User, Server, Password, Mod) -> db_try_register(User, Server, PlainPassword, Passwords, Mod) ->
case erlang:function_exported(Mod, try_register, 3) of Ret = case erlang:function_exported(Mod, try_register_multiple, 3) of
true -> true ->
Password1 = case Mod:store_type(Server) of case use_cache(Mod, Server) of
scram -> password_to_scram(Server, Password); true ->
_ -> Password ets_cache:update(
end, cache_tab(Mod), {User, Server}, {ok, Passwords},
Ret = case use_cache(Mod, Server) of fun() -> Mod:try_register_multiple(User, Server, Passwords) end,
true -> cache_nodes(Mod, Server));
ets_cache:update( false ->
cache_tab(Mod), {User, Server}, {ok, Password}, ets_cache:untag(Mod:try_register_multiple(User, Server, Passwords))
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
end; 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. end.
-spec db_set_password(binary(), binary(), password(), module()) -> ok | {error, db_failure | not_allowed}. -spec db_set_password(binary(), binary(), binary(), [password()], module()) -> ok | {error, db_failure | not_allowed}.
db_set_password(User, Server, Password, Mod) -> db_set_password(User, Server, PlainPassword, Passwords, Mod) ->
case erlang:function_exported(Mod, set_password, 3) of Ret = case erlang:function_exported(Mod, set_password_multiple, 3) of
true -> true ->
Password1 = case Mod:store_type(Server) of case use_cache(Mod, Server) of
scram -> password_to_scram(Server, Password);
_ -> Password
end,
Ret = case use_cache(Mod, Server) of
true -> true ->
ets_cache:update( ets_cache:update(
cache_tab(Mod), {User, Server}, {ok, Password}, cache_tab(Mod), {User, Server}, {ok, Passwords},
fun() -> Mod:set_password(User, Server, Password1) end, fun() -> Mod:set_password_multiple(User, Server, Passwords) end,
cache_nodes(Mod, Server)); cache_nodes(Mod, Server));
false -> false ->
ets_cache:untag(Mod:set_password(User, Server, Password1)) ets_cache:untag(Mod:set_password_multiple(User, Server, Passwords))
end, end;
case Ret of _ ->
{ok, _} -> ok; case erlang:function_exported(Mod, set_password, 3) of
{error, _} = Err -> Err true ->
end; case use_cache(Mod, Server) of
false -> true ->
{error, not_allowed} 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. end.
db_get_password(User, Server, Mod) -> db_get_password(User, Server, Mod) ->
@ -655,10 +773,20 @@ db_get_password(User, Server, Mod) ->
error; error;
true when UseCache -> true when UseCache ->
ets_cache:lookup( ets_cache:lookup(
cache_tab(Mod), {User, Server}, cache_tab(Mod), {User, Server},
fun() -> Mod:get_password(User, Server) end); 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 -> 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. end.
db_user_exists(User, Server, Mod) -> db_user_exists(User, Server, Mod) ->
@ -703,8 +831,8 @@ db_user_exists(User, Server, Mod) ->
db_check_password(User, AuthzId, Server, ProvidedPassword, db_check_password(User, AuthzId, Server, ProvidedPassword,
Digest, DigestFun, Mod) -> Digest, DigestFun, Mod) ->
case db_get_password(User, Server, Mod) of case db_get_password(User, Server, Mod) of
{ok, ValidPassword} -> {ok, ValidPasswords} ->
match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun); match_passwords(ProvidedPassword, ValidPasswords, Digest, DigestFun);
error -> error ->
case {Mod:store_type(Server), use_cache(Mod, Server)} of case {Mod:store_type(Server), use_cache(Mod, Server)} of
{external, true} -> {external, true} ->
@ -809,7 +937,9 @@ password_to_scram(Host, Password) ->
password_to_scram(_Host, #scram{} = Password, _IterationCount) -> password_to_scram(_Host, #scram{} = Password, _IterationCount) ->
Password; Password;
password_to_scram(Host, Password, IterationCount) -> 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), Salt = p1_rand:bytes(?SALT_LENGTH),
SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount),
StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)),
@ -897,11 +1027,19 @@ auth_modules(Server) ->
misc:atom_to_binary(M)]) misc:atom_to_binary(M)])
|| M <- Methods]. || M <- Methods].
-spec match_passwords(password(), password(), -spec match_passwords(password(), [password()],
binary(), digest_fun() | undefined) -> boolean(). 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); 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), StoredKey = base64:decode(Scram#scram.storedkey),
DigRes = if Digest /= <<"">> -> DigRes = if Digest /= <<"">> ->
Digest == DigestFun(StoredKey); Digest == DigestFun(StoredKey);
@ -912,9 +1050,9 @@ match_passwords(Password, #scram{} = Scram, Digest, DigestFun) ->
true -> true ->
StoredKey == Password andalso Password /= <<"">> StoredKey == Password andalso Password /= <<"">>
end; end;
match_passwords(ProvidedPassword, ValidPassword, <<"">>, undefined) -> match_password(ProvidedPassword, ValidPassword, <<"">>, undefined) ->
ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>; ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>;
match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun) -> match_password(ProvidedPassword, ValidPassword, Digest, DigestFun) ->
DigRes = if Digest /= <<"">> -> DigRes = if Digest /= <<"">> ->
Digest == DigestFun(ValidPassword); Digest == DigestFun(ValidPassword);
true -> false true -> false
@ -983,7 +1121,7 @@ convert_to_scram(Server) ->
lists:foreach( lists:foreach(
fun({U, S}) -> fun({U, S}) ->
case get_password(U, S) of case get_password(U, S) of
Pass when is_binary(Pass) -> [Pass] when is_binary(Pass) ->
SPass = password_to_scram(Server, Pass), SPass = password_to_scram(Server, Pass),
set_password(U, S, SPass); set_password(U, S, SPass);
_ -> _ ->

View file

@ -29,11 +29,11 @@
-behaviour(ejabberd_auth). -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, get_users/2, init_db/0,
count_users/2, get_password/2, count_users/2, get_password/2,
remove_user/2, store_type/1, import/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]). -export([need_transform/1, transform/1]).
-include("logger.hrl"). -include("logger.hrl").
@ -86,30 +86,58 @@ plain_password_required(Server) ->
store_type(Server) -> store_type(Server) ->
ejabberd_auth:password_format(Server). ejabberd_auth:password_format(Server).
set_password(User, Server, Password) -> set_password_multiple(User, Server, Passwords) ->
US = {User, Server}, F = fun() ->
F = fun () -> lists:foreach(
mnesia:write(#passwd{us = US, password = Password}) 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, end,
case mnesia:transaction(F) of case mnesia:transaction(F) of
{atomic, ok} -> {atomic, ok} ->
{cache, {ok, Password}}; {cache, {ok, Passwords}};
{aborted, Reason} -> {aborted, Reason} ->
?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]),
{nocache, {error, db_failure}} {nocache, {error, db_failure}}
end. end.
try_register(User, Server, Password) -> set_password_instance(User, Server, Password) ->
US = {User, Server}, F = fun() ->
F = fun () -> case Password of
case mnesia:read({passwd, US}) of #scram{hash = Hash} = Password ->
[] -> mnesia:write(#passwd{us = {User, Server, Hash}, password = Password});
mnesia:write(#passwd{us = US, password = Password}), Plain ->
mnesia:dirty_update_counter(reg_users_counter, Server, 1), mnesia:write(#passwd{us = {User, Server, plain}, password = Plain})
{ok, Password}; end
[_] -> end,
{error, exists} case mnesia:transaction(F) of
end {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, end,
case mnesia:transaction(F) of case mnesia:transaction(F) of
{atomic, Res} -> {atomic, Res} ->
@ -120,9 +148,10 @@ try_register(User, Server, Password) ->
end. end.
get_users(Server, []) -> get_users(Server, []) ->
mnesia:dirty_select(passwd, Users = mnesia:dirty_select(passwd,
[{#passwd{us = '$1', _ = '_'}, [{#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}]) get_users(Server, [{from, Start}, {to, End}])
when is_integer(Start) and is_integer(End) -> when is_integer(Start) and is_integer(End) ->
get_users(Server, [{limit, End - Start + 1}, {offset, Start}]); get_users(Server, [{limit, End - Start + 1}, {offset, Start}]);
@ -179,22 +208,48 @@ count_users(Server, _) ->
count_users(Server, []). count_users(Server, []).
get_password(User, Server) -> get_password(User, Server) ->
case mnesia:dirty_read(passwd, {User, Server}) of case mnesia:dirty_select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'},
[{passwd, _, {scram, SK, SEK, Salt, IC}}] -> [{'==', '$1', User},
{cache, {ok, #scram{storedkey = SK, serverkey = SEK, {'==', '$2', Server}],
salt = Salt, hash = sha, iterationcount = IC}}}; ['$3']}]) of
[#passwd{password = Password}] -> [_|_] = List ->
{cache, {ok, Password}}; 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} {cache, error}
end. 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) -> remove_user(User, Server) ->
US = {User, Server},
F = fun () -> F = fun () ->
mnesia:delete({passwd, US}), Keys = mnesia:select(passwd, [{{'_', '$1', '_'},
mnesia:dirty_update_counter(reg_users_counter, Server, -1), [{'==', {element, 1, '$1'}, User},
ok {'==', {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, end,
case mnesia:transaction(F) of case mnesia:transaction(F) of
{atomic, ok} -> {atomic, ok} ->
@ -206,45 +261,10 @@ remove_user(User, Server) ->
need_transform(#reg_users_counter{}) -> need_transform(#reg_users_counter{}) ->
false; false;
need_transform({passwd, {U, S}, Pass}) -> need_transform({passwd, {_U, _S, _T}, _Pass}) ->
case Pass of false;
_ when is_binary(Pass) -> need_transform({passwd, {_U, _S}, _Pass}) ->
case store_type(S) of true.
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.
transform({passwd, {U, S}, Pass}) transform({passwd, {U, S}, Pass})
when is_list(U) orelse is_list(S) orelse is_list(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 = NewUS, password = NewPass});
transform(#passwd{us = {U, S}, password = Password} = P) transform(#passwd{us = {U, S}, password = Password} = P)
when is_binary(Password) -> when is_binary(Password) ->
case store_type(S) of P#passwd{us = {U, S, plain}, password = Password};
scram -> transform({passwd, {U, S}, {scram, SK, SEK, Salt, IC}}) ->
case jid:resourceprep(Password) of #passwd{us = {U, S, sha},
error -> password = #scram{storedkey = SK, serverkey = SEK,
?ERROR_MSG("SASLprep failed for password of user ~ts@~ts", salt = Salt, hash = sha, iterationcount = IC}};
[U, S]), transform(#passwd{us = {U, S}, password = #scram{hash = Hash}} = P) ->
P; P#passwd{us = {U, S, Hash}};
_ -> transform(Other) -> Other.
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.
import(LServer, [LUser, Password, _TimeStamp]) -> import(LServer, [LUser, Password, _TimeStamp]) ->
mnesia:dirty_write( mnesia:dirty_write(

View file

@ -30,10 +30,10 @@
-behaviour(ejabberd_auth). -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, get_users/2, count_users/2, get_password/2,
remove_user/2, store_type/1, plain_password_required/1, 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]). -export([sql_schemas/0]).
-include_lib("xmpp/include/scram.hrl"). -include_lib("xmpp/include/scram.hrl").
@ -49,7 +49,34 @@ start(Host) ->
ok. ok.
sql_schemas() -> 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, version = 1,
tables = tables =
[#sql_table{ [#sql_table{
@ -78,42 +105,87 @@ plain_password_required(Server) ->
store_type(Server) -> store_type(Server) ->
ejabberd_auth:password_format(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 = F =
fun() -> fun() ->
case Password of ejabberd_sql:sql_query_t(
#scram{hash = Hash, storedkey = SK, serverkey = SEK, ?SQL("delete from users where username=%(User)s and %(Server)H")),
salt = Salt, iterationcount = IC} -> lists:foreach(
SK2 = scram_hash_encode(Hash, SK), fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK,
salt = Salt, iterationcount = IC}) ->
set_password_scram_t( set_password_scram_t(
User, Server, User, Server, Hash,
SK2, SEK, Salt, IC); SK, SEK, Salt, IC);
_ -> (Plain) ->
set_password_t(User, Server, Password) set_password_t(User, Server, Plain)
end end, Passwords)
end, end,
case ejabberd_sql:sql_transaction(Server, F) of case ejabberd_sql:sql_transaction(Server, F) of
{atomic, _} -> {atomic, _} ->
{cache, {ok, Password}}; {cache, {ok, Passwords}};
{aborted, _} -> {aborted, _} ->
{nocache, {error, db_failure}} {nocache, {error, db_failure}}
end. end.
try_register(User, Server, Password) -> try_register_multiple(User, Server, Passwords) ->
Res = F =
case Password of fun() ->
#scram{hash = Hash, storedkey = SK, serverkey = SEK, case ejabberd_sql:sql_query_t(
salt = Salt, iterationcount = IC} -> ?SQL("select @(count(*))d from users where username=%(User)s and %(Server)H")) of
SK2 = scram_hash_encode(Hash, SK), {selected, [{0}]} ->
add_user_scram( lists:foreach(
Server, User, fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK,
SK2, SEK, Salt, IC); salt = Salt, iterationcount = IC}) ->
_ -> set_password_scram_t(
add_user(Server, User, Password) User, Server, Hash,
end, SK, SEK, Salt, IC);
case Res of (Plain) ->
{updated, 1} -> {cache, {ok, Password}}; set_password_t(User, Server, Plain)
_ -> {nocache, {error, exists}} 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. end.
get_users(Server, Opts) -> get_users(Server, Opts) ->
@ -132,21 +204,41 @@ count_users(Server, Opts) ->
get_password(User, Server) -> get_password(User, Server) ->
case get_password_scram(Server, User) of 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, []} -> {selected, []} ->
{cache, error}; {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} {nocache, error}
end. end.
@ -159,6 +251,13 @@ remove_user(User, Server) ->
{error, db_failure} {error, db_failure}
end. 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) -> scram_hash_encode(Hash, StoreKey) ->
case Hash of case Hash of
sha -> StoreKey; sha -> StoreKey;
@ -166,12 +265,14 @@ scram_hash_encode(Hash, StoreKey) ->
sha512 -> <<"sha512:", StoreKey/binary>> sha512 -> <<"sha512:", StoreKey/binary>>
end. end.
set_password_scram_t(LUser, LServer, set_password_scram_t(LUser, LServer, Hash,
StoredKey, ServerKey, Salt, IterationCount) -> StoredKey, ServerKey, Salt, IterationCount) ->
Type = hash_to_num(Hash),
?SQL_UPSERT_T( ?SQL_UPSERT_T(
"users", "users",
["!username=%(LUser)s", ["!username=%(LUser)s",
"!server_host=%(LServer)s", "!server_host=%(LServer)s",
"!type=%(Type)d",
"password=%(StoredKey)s", "password=%(StoredKey)s",
"serverkey=%(ServerKey)s", "serverkey=%(ServerKey)s",
"salt=%(Salt)s", "salt=%(Salt)s",
@ -182,40 +283,31 @@ set_password_t(LUser, LServer, Password) ->
"users", "users",
["!username=%(LUser)s", ["!username=%(LUser)s",
"!server_host=%(LServer)s", "!server_host=%(LServer)s",
"!type=1",
"password=%(Password)s", "password=%(Password)s",
"serverkey=''", "serverkey=''",
"salt=''", "salt=''",
"iterationcount=0"]). "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) -> get_password_scram(LServer, LUser) ->
ejabberd_sql:sql_query( ejabberd_sql:sql_query(
LServer, 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" " from users"
" where username=%(LUser)s and %(LServer)H")). " 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) -> del_user(LServer, LUser) ->
ejabberd_sql:sql_query( ejabberd_sql:sql_query(
LServer, LServer,
@ -224,7 +316,7 @@ del_user(LServer, LUser) ->
list_users(LServer, []) -> list_users(LServer, []) ->
ejabberd_sql:sql_query( ejabberd_sql:sql_query(
LServer, 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}]) list_users(LServer, [{from, Start}, {to, End}])
when is_integer(Start) and is_integer(End) -> when is_integer(Start) and is_integer(End) ->
list_users(LServer, list_users(LServer,
@ -240,7 +332,7 @@ list_users(LServer, [{limit, Limit}, {offset, Offset}])
when is_integer(Limit) and is_integer(Offset) -> when is_integer(Limit) and is_integer(Offset) ->
ejabberd_sql:sql_query( ejabberd_sql:sql_query(
LServer, LServer,
?SQL("select @(username)s from users " ?SQL("select @(distinct username)s from users "
"where %(LServer)H " "where %(LServer)H "
"order by username " "order by username "
"limit %(Limit)d offset %(Offset)d")); "limit %(Limit)d offset %(Offset)d"));
@ -252,7 +344,7 @@ list_users(LServer,
SPrefix2 = <<SPrefix/binary, $%>>, SPrefix2 = <<SPrefix/binary, $%>>,
ejabberd_sql:sql_query( ejabberd_sql:sql_query(
LServer, LServer,
?SQL("select @(username)s from users " ?SQL("select @(distinct username)s from users "
"where username like %(SPrefix2)s %ESCAPE and %(LServer)H " "where username like %(SPrefix2)s %ESCAPE and %(LServer)H "
"order by username " "order by username "
"limit %(Limit)d offset %(Offset)d")). "limit %(Limit)d offset %(Offset)d")).
@ -269,11 +361,11 @@ users_number(LServer) ->
" where oid = 'users'::regclass::oid")); " where oid = 'users'::regclass::oid"));
_ -> _ ->
ejabberd_sql:sql_query_t( 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; end;
(_Type, _) -> (_Type, _) ->
ejabberd_sql:sql_query_t( 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). end).
users_number(LServer, [{prefix, Prefix}]) users_number(LServer, [{prefix, Prefix}])
@ -282,7 +374,7 @@ users_number(LServer, [{prefix, Prefix}])
SPrefix2 = <<SPrefix/binary, $%>>, SPrefix2 = <<SPrefix/binary, $%>>,
ejabberd_sql:sql_query( ejabberd_sql:sql_query(
LServer, LServer,
?SQL("select @(count(*))d from users " ?SQL("select @(count(distinct username))d from users "
"where username like %(SPrefix2)s %ESCAPE and %(LServer)H")); "where username like %(SPrefix2)s %ESCAPE and %(LServer)H"));
users_number(LServer, []) -> users_number(LServer, []) ->
users_number(LServer). users_number(LServer).
@ -290,7 +382,7 @@ users_number(LServer, []) ->
which_users_exists(LServer, LUsers) when length(LUsers) =< 100 -> which_users_exists(LServer, LUsers) when length(LUsers) =< 100 ->
try ejabberd_sql:sql_query( try ejabberd_sql:sql_query(
LServer, 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} -> {selected, Matching} ->
[U || {U} <- Matching]; [U || {U} <- Matching];
{error, _} = E -> {error, _} = E ->

View file

@ -423,16 +423,27 @@ sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = St
Type = ejabberd_auth:store_type(LServer), Type = ejabberd_auth:store_type(LServer),
Mechs1 = ejabberd_option:disable_sasl_mechanisms(LServer), Mechs1 = ejabberd_option:disable_sasl_mechanisms(LServer),
ScramHash = ejabberd_option:auth_scram_hash(LServer), {Digest, ShaAv, Sha256Av, Sha512Av} =
ShaAv = Type == plain orelse (Type == scram andalso ScramHash == sha), case ejabberd_option:auth_stored_password_types(LServer) of
Sha256Av = Type == plain orelse (Type == scram andalso ScramHash == sha256), [] ->
Sha512Av = Type == plain orelse (Type == scram andalso ScramHash == sha512), 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 %% I re-created it from cyrsasl ets magic, but I think it's wrong
%% TODO: need to check before 18.09 release %% TODO: need to check before 18.09 release
lists:filter( lists:filter(
fun(<<"ANONYMOUS">>) -> fun(<<"ANONYMOUS">>) ->
ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer); ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer);
(<<"DIGEST-MD5">>) -> Type == plain; (<<"DIGEST-MD5">>) -> Digest;
(<<"SCRAM-SHA-1">>) -> ShaAv; (<<"SCRAM-SHA-1">>) -> ShaAv;
(<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted; (<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted;
(<<"SCRAM-SHA-256">>) -> Sha256Av; (<<"SCRAM-SHA-256">>) -> Sha256Av;

View file

@ -19,6 +19,7 @@
-export([auth_opts/0, auth_opts/1]). -export([auth_opts/0, auth_opts/1]).
-export([auth_password_format/0, auth_password_format/1]). -export([auth_password_format/0, auth_password_format/1]).
-export([auth_scram_hash/0, auth_scram_hash/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([auth_use_cache/0, auth_use_cache/1]).
-export([c2s_cafile/0, c2s_cafile/1]). -export([c2s_cafile/0, c2s_cafile/1]).
-export([c2s_ciphers/0, c2s_ciphers/1]). -export([c2s_ciphers/0, c2s_ciphers/1]).
@ -264,6 +265,13 @@ auth_scram_hash() ->
auth_scram_hash(Host) -> auth_scram_hash(Host) ->
ejabberd_config:get_option({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(). -spec auth_use_cache() -> boolean().
auth_use_cache() -> auth_use_cache() ->
auth_use_cache(global). auth_use_cache(global).

View file

@ -77,6 +77,8 @@ opt_type(auth_opts) ->
{path_prefix, V} {path_prefix, V}
end, L) end, L)
end; end;
opt_type(auth_stored_password_types) ->
econf:list(econf:enum([plain, scram_sha1, scram_sha256, scram_sha512]));
opt_type(auth_password_format) -> opt_type(auth_password_format) ->
econf:enum([plain, scram]); econf:enum([plain, scram]);
opt_type(auth_scram_hash) -> opt_type(auth_scram_hash) ->
@ -546,6 +548,7 @@ options() ->
{auth_opts, []}, {auth_opts, []},
{auth_password_format, plain}, {auth_password_format, plain},
{auth_scram_hash, sha}, {auth_scram_hash, sha},
{auth_stored_password_types, []},
{auth_external_user_exists_check, true}, {auth_external_user_exists_check, true},
{auth_use_cache, {auth_use_cache,
fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end},

View file

@ -399,6 +399,14 @@ doc() ->
"You shouldn't change this if you already have passwords generated with " "You shouldn't change this if you already have passwords generated with "
"a different algorithm - users that have such passwords will not be able " "a different algorithm - users that have such passwords will not be able "
"to authenticate. The default value is 'sha'.")}}, "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, {auth_external_user_exists_check,
#{value => "true | false", #{value => "true | false",
note => "added in 23.10", note => "added in 23.10",

View file

@ -107,7 +107,7 @@ c2s_handle_sasl2_task_data({_, #{user := User, server := Server,
#scram_upgrade_hash{data = SaltedPassword} -> #scram_upgrade_hash{data = SaltedPassword} ->
StoredKey = scram:stored_key(Algo, scram:client_key(Algo, SaltedPassword)), StoredKey = scram:stored_key(Algo, scram:client_key(Algo, SaltedPassword)),
ServerKey = scram:server_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, #scram{hash = Algo, iterationcount = Iter, salt = Salt,
serverkey = ServerKey, storedkey = StoredKey}), serverkey = ServerKey, storedkey = StoredKey}),
State2 = maps:remove(scram_upgrade, State), State2 = maps:remove(scram_upgrade, State),

View file

@ -78,7 +78,7 @@ adduser(Config) ->
"server/" ++ binary_to_list(Server) ++ "/users/", "server/" ++ binary_to_list(Server) ++ "/users/",
<<"register/user=", (mue(User))/binary, "&register/password=", <<"register/user=", (mue(User))/binary, "&register/password=",
(mue(Password))/binary, "&register=Register">>), (mue(Password))/binary, "&register=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, ?match({_, _}, binary:match(Body, <<"User ", User/binary, "@", Server/binary,
" successfully registered">>)). " successfully registered">>)).
@ -92,7 +92,7 @@ changepassword(Config) ->
++ "/user/" ++ binary_to_list(mue(User)) ++ "/", ++ "/user/" ++ binary_to_list(mue(User)) ++ "/",
<<"change_password/newpass=", (mue(Password))/binary, <<"change_password/newpass=", (mue(Password))/binary,
"&change_password=Change+Password">>), "&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, <<"<div class='result'><code>ok</code></div>">>)). ?match({_, _}, binary:match(Body, <<"<div class='result'><code>ok</code></div>">>)).
removeuser(Config) -> removeuser(Config) ->