%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_mnesia.erl %%% Author : Alexey Shchepin %%% Purpose : Authentication via mnesia %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2026 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth_mnesia). -author('alexey@process-one.net'). -behaviour(ejabberd_auth). -behaviour(ejabberd_db_serialize). -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, drop_password_type/2, set_password_instance/3]). -export([need_transform/1, transform/1]). -export([serialize/3, deserialize_start/1, deserialize/2]). -include_lib("stdlib/include/ms_transform.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/scram.hrl"). -include("ejabberd_auth.hrl"). -include("ejabberd_db_serialize.hrl"). -record(reg_users_counter, {vhost = <<"">> :: binary(), count = 0 :: integer() | '$1'}). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host) -> init_db(), update_reg_users_counter_table(Host), ok. stop(_Host) -> ok. init_db() -> ejabberd_mnesia:create(?MODULE, passwd, [{disc_only_copies, [node()]}, {attributes, record_info(fields, passwd)}]), ejabberd_mnesia:create(?MODULE, reg_users_counter, [{ram_copies, [node()]}, {attributes, record_info(fields, reg_users_counter)}]). update_reg_users_counter_table(Server) -> Set = get_users(Server, []), Size = length(Set), LServer = jid:nameprep(Server), F = fun () -> mnesia:write(#reg_users_counter{vhost = LServer, count = Size}) end, mnesia:sync_dirty(F). use_cache(Host) -> case mnesia:table_info(passwd, storage_type) of disc_only_copies -> ejabberd_option:auth_use_cache(Host); _ -> false end. plain_password_required(Server) -> store_type(Server) == scram. store_type(Server) -> ejabberd_auth:password_format(Server). 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, Passwords}}; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {nocache, {error, db_failure}} 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} -> {cache, Res}; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {nocache, {error, db_failure}} end. get_users(Server, []) -> Users = mnesia:dirty_select(passwd, [{#passwd{us = '$1', _ = '_'}, [{'==', {element, 2, '$1'}, Server}], ['$1']}]), lists:uniq([{U, S} || {U, S, _} <- 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}]); get_users(Server, [{limit, Limit}, {offset, Offset}]) when is_integer(Limit) and is_integer(Offset) -> case get_users(Server, []) of [] -> []; Users -> Set = lists:keysort(1, Users), L = length(Set), Start = if Offset < 1 -> 1; Offset > L -> L; true -> Offset end, lists:sublist(Set, Start, Limit) end; get_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], lists:keysort(1, Set); get_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}]) when is_binary(Prefix) and is_integer(Start) and is_integer(End) -> get_users(Server, [{prefix, Prefix}, {limit, End - Start + 1}, {offset, Start}]); get_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) when is_binary(Prefix) and is_integer(Limit) and is_integer(Offset) -> case [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)] of [] -> []; Users -> Set = lists:keysort(1, Users), L = length(Set), Start = if Offset < 1 -> 1; Offset > L -> L; true -> Offset end, lists:sublist(Set, Start, Limit) end; get_users(Server, _) -> get_users(Server, []). count_users(Server, []) -> case mnesia:dirty_select( reg_users_counter, [{#reg_users_counter{vhost = Server, count = '$1'}, [], ['$1']}]) of [Count] -> Count; _ -> 0 end; count_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], length(Set); count_users(Server, _) -> count_users(Server, []). get_password(User, Server) -> 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) -> F = fun () -> 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} -> ok; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, db_failure} end. need_transform(#reg_users_counter{}) -> false; 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) -> NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, NewPass = case Pass of #scram{storedkey = StoredKey, serverkey = ServerKey, salt = Salt} -> Pass#scram{ storedkey = iolist_to_binary(StoredKey), serverkey = iolist_to_binary(ServerKey), salt = iolist_to_binary(Salt)}; _ -> iolist_to_binary(Pass) end, transform(#passwd{us = NewUS, password = NewPass}); transform(#passwd{us = {U, S}, password = Password} = P) when is_binary(Password) -> 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( #passwd{us = {LUser, LServer}, password = Password}). serialize(LServer, BatchSize, undefined) -> Users = lists:uniq(lists:sort(mnesia:dirty_select(passwd, ets:fun2ms( fun(#passwd{us = {U, S, _}}) when S == LServer -> U end)))), ReadPass = fun(U) -> Passwords = mnesia:dirty_select(passwd, ets:fun2ms( fun(#passwd{us = {U2, S, _}, password = Pass}) when S == LServer, U2 == U -> Pass end)), P = lists:map( fun(#scram{hash = Hash, salt = Salt, storedkey = SK, serverkey = SEK, iterationcount = IC}) -> {Hash, SK, SEK, Salt, IC}; (Plain) -> Plain end, Passwords), {ok, #serialize_auth_v1{serverhost = LServer, username = U, passwords = P}} end, ejabberd_db_serialize:iter_records([{ fun() -> case Users of [H | T] -> {ok, H, T}; _ -> fin end end, fun([]) -> fin; ([H | T]) -> {ok, H, T} end, ReadPass}], [], BatchSize); serialize(_LServer, BatchSize, Key) -> ejabberd_db_serialize:iter_records(Key, [], BatchSize). deserialize_start(LServer) -> mnesia:transaction( fun() -> Keys = mnesia:select(passwd, ets:fun2ms( fun(#passwd{us = US}) when element(2, US) == LServer -> US end)), lists:foreach(fun(Key) -> mnesia:delete(passwd, Key, write) end, Keys) end), ok. deserialize(LServer, Batch) -> F = fun() -> lists:foreach( fun(#serialize_auth_v1{username = U, passwords = P}) -> lists:foreach( fun({Hash, SK, SEK, Salt, IC}) -> mnesia:write(#passwd{us = {U, LServer, Hash}, password = #scram{hash = Hash, serverkey = SEK, storedkey = SK, salt = Salt, iterationcount = IC}}); (Plain) -> mnesia:write(#passwd{us = {U, LServer, plain}, password = Plain}) end, P) end, Batch) end, case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> {error, io_lib:format("Error when writing passwords data: ~p", [Reason])} end.