From: Badlop Date: Mon, 15 Aug 2011 22:25:03 +0000 (+0200) Subject: Preliminary patch for SASL SCRAM-SHA-1 (thanks to Stephen Röttger)(EJAB-1196) X-Git-Tag: v2.1.9~86 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=e80b92b48148505b44c6a378db36badfe60fce79;p=ejabberd Preliminary patch for SASL SCRAM-SHA-1 (thanks to Stephen Röttger)(EJAB-1196) --- diff --git a/src/cyrsasl.erl b/src/cyrsasl.erl index fd54ce44f..627ee4d32 100644 --- a/src/cyrsasl.erl +++ b/src/cyrsasl.erl @@ -34,7 +34,7 @@ server_start/3, server_step/2]). --record(sasl_mechanism, {mechanism, module, require_plain_password}). +-record(sasl_mechanism, {mechanism, module, password_type}). -record(sasl_state, {service, myname, realm, get_password, check_password, check_password_digest, mech_mod, mech_state}). @@ -52,14 +52,15 @@ start() -> {keypos, #sasl_mechanism.mechanism}]), cyrsasl_plain:start([]), cyrsasl_digest:start([]), + cyrsasl_scram:start([]), cyrsasl_anonymous:start([]), ok. -register_mechanism(Mechanism, Module, RequirePlainPassword) -> +register_mechanism(Mechanism, Module, PasswordType) -> ets:insert(sasl_mechanism, #sasl_mechanism{mechanism = Mechanism, module = Module, - require_plain_password = RequirePlainPassword}). + password_type = PasswordType}). %%% TODO: use callbacks %%-include("ejabberd.hrl"). @@ -97,17 +98,17 @@ check_credentials(_State, Props) -> end. listmech(Host) -> - RequirePlainPassword = ejabberd_auth:plain_password_required(Host), - Mechs = ets:select(sasl_mechanism, [{#sasl_mechanism{mechanism = '$1', - require_plain_password = '$2', + password_type = '$2', _ = '_'}, - if - RequirePlainPassword -> - [{'==', '$2', false}]; - true -> - [] + case ejabberd_auth:storage_type(Host) of + external -> + [{'==', '$2', plain}]; + scram -> + [{'/=', '$2', digest}]; + _Else -> + [] end, ['$1']}]), filter_anonymous(Host, Mechs). @@ -152,6 +153,13 @@ server_step(State, ClientIn) -> {error, Error} -> {error, Error} end; + {ok, Props, ServerOut} -> + case check_credentials(State, Props) of + ok -> + {ok, Props, ServerOut}; + {error, Error} -> + {error, Error} + end; {continue, ServerOut, NewMechState} -> {continue, ServerOut, State#sasl_state{mech_state = NewMechState}}; diff --git a/src/cyrsasl_anonymous.erl b/src/cyrsasl_anonymous.erl index 3af9991d1..1a61d9176 100644 --- a/src/cyrsasl_anonymous.erl +++ b/src/cyrsasl_anonymous.erl @@ -34,7 +34,7 @@ -record(state, {server}). start(_Opts) -> - cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, false), + cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, plain), ok. stop() -> diff --git a/src/cyrsasl_digest.erl b/src/cyrsasl_digest.erl index 40e7edfd8..eea64cb48 100644 --- a/src/cyrsasl_digest.erl +++ b/src/cyrsasl_digest.erl @@ -40,7 +40,7 @@ host}). start(_Opts) -> - cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, true). + cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, digest). stop() -> ok. diff --git a/src/cyrsasl_plain.erl b/src/cyrsasl_plain.erl index 1b7108e31..a8913d3a3 100644 --- a/src/cyrsasl_plain.erl +++ b/src/cyrsasl_plain.erl @@ -34,7 +34,7 @@ -record(state, {check_password}). start(_Opts) -> - cyrsasl:register_mechanism("PLAIN", ?MODULE, false), + cyrsasl:register_mechanism("PLAIN", ?MODULE, plain), ok. stop() -> diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl new file mode 100644 index 000000000..8b6e6a963 --- /dev/null +++ b/src/cyrsasl_scram.erl @@ -0,0 +1,168 @@ +-module(cyrsasl_scram). +-author('stephen.roettger@googlemail.com'). + +-export([start/1, + stop/0, + mech_new/4, + mech_step/2]). + +-include("ejabberd.hrl"). + +-behaviour(cyrsasl). + +-record(state, {step, stored_key, server_key, username, get_password, check_password, + auth_message, client_nonce, server_nonce}). + +-define(DEFAULT_ITERATION_COUNT, 4096). +-define(SALT_LENGTH, 16). +-define(NONCE_LENGTH, 16). + +start(_Opts) -> + cyrsasl:register_mechanism("SCRAM-SHA-1", ?MODULE, scram). + +stop() -> + ok. + +mech_new(_Host, GetPassword, _CheckPassword, _CheckPasswordDigest) -> + {ok, #state{step = 2, get_password = GetPassword}}. + +mech_step(#state{step = 2} = State, ClientIn) -> + case string:tokens(ClientIn, ",") of + ["n", UserNameAttribute, ClientNonceAttribute] -> + case parse_attribute(UserNameAttribute) of + {$n, EscapedUserName} -> + case unescape_username(EscapedUserName) of + error -> + {error, "protocol-error-bad-username"}; + UserName -> + case parse_attribute(ClientNonceAttribute) of + {$r, ClientNonce} -> + case (State#state.get_password)(UserName) of + {false, _} -> + {error, "not-authorized", UserName}; + {Ret, _AuthModule} -> + {StoredKey, ServerKey, Salt, IterationCount} = if + is_tuple(Ret) -> + Ret; + true -> + TempSalt = crypto:rand_bytes(?SALT_LENGTH), + SaltedPassword = scram:salted_password(Ret, TempSalt, ?DEFAULT_ITERATION_COUNT), + {scram:stored_key(scram:client_key(SaltedPassword)), TempSalt, ?DEFAULT_ITERATION_COUNT} + end, + ClientFirstMessageBare = string:substr(ClientIn, string:str(ClientIn, "n=")), + ServerNonce = base64:encode_to_string(crypto:rand_bytes(?NONCE_LENGTH)), + ServerFirstMessage = "r=" ++ ClientNonce ++ ServerNonce ++ "," ++ + "s=" ++ base64:encode_to_string(Salt) ++ "," ++ + "i=" ++ integer_to_list(IterationCount), + {continue, + ServerFirstMessage, + State#state{step = 4, stored_key = StoredKey, server_key = ServerKey, + auth_message = ClientFirstMessageBare ++ "," ++ ServerFirstMessage, + client_nonce = ClientNonce, server_nonce = ServerNonce, username = UserName}} + end; + _Else -> + {error, "not-supported"} + end + end; + {error, Reason} -> + {error, Reason}; + _Else -> + {error, "bad-protocol"} + end; + _Else -> + {error, "bad-protocol"} + end; +mech_step(#state{step = 4} = State, ClientIn) -> + case string:tokens(ClientIn, ",") of + [GS2ChannelBindingAttribute, NonceAttribute, ClientProofAttribute] -> + case parse_attribute(GS2ChannelBindingAttribute) of + {$c, "biws"} -> %biws is base64 for n,, => channelbinding not supported + Nonce = State#state.client_nonce ++ State#state.server_nonce, + case parse_attribute(NonceAttribute) of + {$r, CompareNonce} when CompareNonce == Nonce -> + case parse_attribute(ClientProofAttribute) of + {$p, ClientProofB64} -> + ClientProof = base64:decode(ClientProofB64), + AuthMessage = State#state.auth_message ++ "," ++ string:substr(ClientIn, 1, string:str(ClientIn, ",p=")-1), + ClientSignature = scram:client_signature(State#state.stored_key, AuthMessage), + ClientKey = scram:client_key(ClientProof, ClientSignature), + CompareStoredKey = scram:stored_key(ClientKey), + if CompareStoredKey == State#state.stored_key -> + ServerSignature = scram:server_signature(State#state.server_key, AuthMessage), + {ok, [{username, State#state.username}], "v=" ++ base64:encode_to_string(ServerSignature)}; + true -> + {error, "bad-auth"} + end; + _Else -> + {error, "bad-protocol"} + end; + {$r, _} -> + {error, "bad-nonce"}; + _Else -> + {error, "bad-protocol"} + end; + _Else -> + {error, "bad-protocol"} + end; + _Else -> + {error, "bad-protocol"} + end. + +parse_attribute(Attribute) -> + AttributeLen = string:len(Attribute), + if + AttributeLen > 3 -> + SecondChar = lists:nth(2, Attribute), + case is_alpha(lists:nth(1, Attribute)) of + true -> + if + SecondChar == $= -> + case string:substr(Attribute, 3) of + String when is_list(String) -> + {lists:nth(1, Attribute), String}; + _Else -> + {error, "bad-format failed"} + end; + true -> + {error, "bad-format second char not equal sign"} + end; + _Else -> + {error, "bad-format first char not a letter"} + end; + true -> + {error, "bad-format attribute too short"} + end. + +unescape_username("") -> + ""; +unescape_username(EscapedUsername) -> + Pos = string:str(EscapedUsername, "="), + if + Pos == 0 -> + EscapedUsername; + true -> + Start = string:substr(EscapedUsername, 1, Pos-1), + End = string:substr(EscapedUsername, Pos), + EndLen = string:len(End), + if + EndLen < 3 -> + error; + true -> + case string:substr(End, 1, 3) of + "=2C" -> + Start ++ "," ++ unescape_username(string:substr(End, 4)); + "=3D" -> + Start ++ "=" ++ unescape_username(string:substr(End, 4)); + _Else -> + error + end + end + end. + +is_alpha(Char) when Char >= $a, Char =< $z -> + true; +is_alpha(Char) when Char >= $A, Char =< $Z -> + true; +is_alpha(_) -> + true. + diff --git a/src/ejabberd.app b/src/ejabberd.app index 5ab3b5308..031fdaf46 100644 --- a/src/ejabberd.app +++ b/src/ejabberd.app @@ -10,6 +10,7 @@ cyrsasl, cyrsasl_digest, cyrsasl_plain, + cyrsasl_scram, ejabberd_admin, ejabberd_app, ejabberd_auth_anonymous, diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 23f9c4ba2..d937340f1 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -50,6 +50,7 @@ remove_user/2, remove_user/3, plain_password_required/1, + storage_type/1, entropy/1 ]). @@ -75,6 +76,21 @@ plain_password_required(Server) -> M:plain_password_required() end, auth_modules(Server)). +storage_type(Server) -> + lists:foldl( + fun(_, external) -> + external; + (M, scram) -> + case M:storage_type() of + external -> + external; + _Else -> + scram + end; + (M, plain) -> + M:storage_type() + end, plain, auth_modules(Server)). + %% @doc Check if the user and password can login in server. %% @spec (User::string(), Server::string(), Password::string()) -> %% true | false diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index 787d96b46..d3ecffc1b 100644 --- a/src/ejabberd_auth_anonymous.erl +++ b/src/ejabberd_auth_anonymous.erl @@ -51,6 +51,7 @@ is_user_exists/2, remove_user/2, remove_user/3, + storage_type/0, plain_password_required/0]). -include("ejabberd.hrl"). @@ -247,3 +248,6 @@ remove_user(_User, _Server, _Password) -> plain_password_required() -> false. + +storage_type() -> + plain. diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index a426fe958..b625af84c 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -43,6 +43,7 @@ is_user_exists/2, remove_user/2, remove_user/3, + storage_type/0, plain_password_required/0 ]). @@ -78,6 +79,9 @@ check_cache_last_options(Server) -> plain_password_required() -> true. +storage_type() -> + external. + check_password(User, Server, Password) -> case get_cache_option(Server) of false -> check_password_extauth(User, Server, Password); diff --git a/src/ejabberd_auth_internal.erl b/src/ejabberd_auth_internal.erl index 39554bebc..847737f29 100644 --- a/src/ejabberd_auth_internal.erl +++ b/src/ejabberd_auth_internal.erl @@ -43,6 +43,7 @@ is_user_exists/2, remove_user/2, remove_user/3, + storage_type/0, plain_password_required/0 ]). @@ -77,6 +78,9 @@ update_reg_users_counter_table(Server) -> plain_password_required() -> false. +storage_type() -> + plain. + check_password(User, Server, Password) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), diff --git a/src/ejabberd_auth_internal_scram.erl b/src/ejabberd_auth_internal_scram.erl new file mode 100644 index 000000000..f39ee4fb1 --- /dev/null +++ b/src/ejabberd_auth_internal_scram.erl @@ -0,0 +1,369 @@ +-module(ejabberd_auth_internal_scram). +-author('stephen.roettger@googlemail.com'). + +%% External exports +-export([start/1, + set_password/3, + check_password/3, + check_password/5, + try_register/3, + dirty_get_registered_users/0, + get_vh_registered_users/1, + get_vh_registered_users/2, + get_vh_registered_users_number/1, + get_vh_registered_users_number/2, + get_password/2, + get_password_s/2, + is_user_exists/2, + remove_user/2, + remove_user/3, + storage_type/0, + plain_password_required/0 + ]). + +-include("ejabberd.hrl"). + +-record(passwd, {us, stored_key, salt, iteration_count, server_key}). +-record(reg_users_counter, {vhost, count}). + +-define(DEFAULT_ITERATION_COUNT, 4096). +-define(SALT_LENGTH, 16). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(Host) -> + mnesia:create_table(passwd, [{disc_copies, [node()]}, + {attributes, record_info(fields, passwd)}]), + mnesia:create_table(reg_users_counter, + [{ram_copies, [node()]}, + {attributes, record_info(fields, reg_users_counter)}]), + update_table(), + update_reg_users_counter_table(Host), + ok. + +update_reg_users_counter_table(Server) -> + Set = get_vh_registered_users(Server), + Size = length(Set), + LServer = jlib:nameprep(Server), + F = fun() -> + mnesia:write(#reg_users_counter{vhost = LServer, + count = Size}) + end, + mnesia:sync_dirty(F). + +plain_password_required() -> + true. + +storage_type() -> + scram. + +check_password(User, Server, Password) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + [UserEntry] = (catch mnesia:dirty_read({passwd, US})), + IterationCount = UserEntry#passwd.iteration_count, + Salt = UserEntry#passwd.salt, + SaltedPassword = scram:salted_password(Password, Salt, IterationCount), + StoredKey = scram:stored_key(scram:client_key(SaltedPassword)), + if + (UserEntry#passwd.stored_key == StoredKey) -> + true; + true -> + false + end. + +check_password(User, Server, Password, Digest, DigestGen) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch mnesia:dirty_read({passwd, US}) of + [#passwd{stored_key = Passwd}] -> + DigRes = if + Digest /= "" -> + Digest == DigestGen(Passwd); + true -> + false + end, + if DigRes -> + true; + true -> + (Passwd == Password) and (Password /= "") + end; + _ -> + false + end. + +%% @spec (User::string(), Server::string(), Password::string()) -> +%% ok | {error, invalid_jid} +set_password(User, Server, Password) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + Salt = crypto:rand_bytes(?SALT_LENGTH), + IterationCount = ?DEFAULT_ITERATION_COUNT, + SaltedPassword = scram:salted_password(Password, Salt, IterationCount), + StoredKey = scram:stored_key(scram:client_key(SaltedPassword)), + ServerKey = scram:server_key(SaltedPassword), + if + (LUser == error) or (LServer == error) -> + {error, invalid_jid}; + true -> + F = fun() -> + mnesia:write(#passwd{us = US, + stored_key = StoredKey, + salt = Salt, + iteration_count = IterationCount, + server_key = ServerKey}) + end, + {atomic, ok} = mnesia:transaction(F), + ok + end. + +%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} | {aborted, Reason} +try_register(User, Server, Password) -> + try_register(User, Server, Password, ?DEFAULT_ITERATION_COUNT). + +try_register(User, Server, Password, IterationCount) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + Salt = crypto:rand_bytes(?SALT_LENGTH), + SaltedPassword = scram:salted_password(Password, Salt, IterationCount), + StoredKey = scram:stored_key(scram:client_key(SaltedPassword)), + ServerKey = scram:server_key(SaltedPassword), + if + (LUser == error) or (LServer == error) -> + {error, invalid_jid}; + true -> + F = fun() -> + case mnesia:read({passwd, US}) of + [] -> + mnesia:write(#passwd{us = US, + stored_key = StoredKey, + salt = Salt, + iteration_count = IterationCount, + server_key = ServerKey}), + mnesia:dirty_update_counter( + reg_users_counter, + LServer, 1), + ok; + [_E] -> + exists + end + end, + mnesia:transaction(F) + end. + +%% Get all registered users in Mnesia +dirty_get_registered_users() -> + mnesia:dirty_all_keys(passwd). + +get_vh_registered_users(Server) -> + LServer = jlib:nameprep(Server), + mnesia:dirty_select( + passwd, + [{#passwd{us = '$1', _ = '_'}, + [{'==', {element, 2, '$1'}, LServer}], + ['$1']}]). + +get_vh_registered_users(Server, [{from, Start}, {to, End}]) + when is_integer(Start) and is_integer(End) -> + get_vh_registered_users(Server, [{limit, End-Start+1}, {offset, Start}]); + +get_vh_registered_users(Server, [{limit, Limit}, {offset, Offset}]) + when is_integer(Limit) and is_integer(Offset) -> + case get_vh_registered_users(Server) of + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) + end; + +get_vh_registered_users(Server, [{prefix, Prefix}]) + when is_list(Prefix) -> + Set = [{U,S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)], + lists:keysort(1, Set); + +get_vh_registered_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}]) + when is_list(Prefix) and is_integer(Start) and is_integer(End) -> + get_vh_registered_users(Server, [{prefix, Prefix}, {limit, End-Start+1}, {offset, Start}]); + +get_vh_registered_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) + when is_list(Prefix) and is_integer(Limit) and is_integer(Offset) -> + case [{U,S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)] of + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) + end; + +get_vh_registered_users(Server, _) -> + get_vh_registered_users(Server). + +get_vh_registered_users_number(Server) -> + LServer = jlib:nameprep(Server), + Query = mnesia:dirty_select( + reg_users_counter, + [{#reg_users_counter{vhost = LServer, count = '$1'}, + [], + ['$1']}]), + case Query of + [Count] -> + Count; + _ -> 0 + end. + +get_vh_registered_users_number(Server, [{prefix, Prefix}]) when is_list(Prefix) -> + Set = [{U, S} || {U, S} <- get_vh_registered_users(Server), lists:prefix(Prefix, U)], + length(Set); + +get_vh_registered_users_number(Server, _) -> + get_vh_registered_users_number(Server). + +get_password(User, Server) -> + %false. + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch mnesia:dirty_read(passwd, US) of + [#passwd{stored_key = Password, server_key = ServerKey, salt = Salt, iteration_count = IterationCount}] -> + {Password, ServerKey, Salt, IterationCount}; + _ -> + false + end. + +get_password_s(User, Server) -> + %"". + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch mnesia:dirty_read(passwd, US) of + [#passwd{stored_key = Password, server_key = ServerKey, salt = Salt, iteration_count = IterationCount}] -> + {Password, ServerKey, Salt, IterationCount}; + _ -> + [] + end. + +%% @spec (User, Server) -> true | false | {error, Error} +is_user_exists(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + case catch mnesia:dirty_read({passwd, US}) of + [] -> + false; + [_] -> + true; + Other -> + {error, Other} + end. + +%% @spec (User, Server) -> ok +%% @doc Remove user. +%% Note: it returns ok even if there was some problem removing the user. +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + F = fun() -> + mnesia:delete({passwd, US}), + mnesia:dirty_update_counter(reg_users_counter, + LServer, -1) + end, + mnesia:transaction(F), + ok. + +%% @spec (User, Server, Password) -> ok | not_exists | not_allowed | bad_request +%% @doc Remove user if the provided password is correct. +remove_user(User, Server, Password) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + F = fun() -> + case mnesia:read({passwd, US}) of + [UserEntry] -> + IterationCount = UserEntry#passwd.iteration_count, + Salt = UserEntry#passwd.salt, + SaltedPassword = scram:salted_password(Password, Salt, IterationCount), + StoredKey = scram:stored_key(scram:client_key(SaltedPassword)), + if + (UserEntry#passwd.stored_key == StoredKey) -> + mnesia:delete({passwd, US}), + mnesia:dirty_update_counter(reg_users_counter, + LServer, -1), + ok; + true -> + not_allowed + end; + _Else -> + not_exists + end + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {atomic, Res} -> + Res; + _ -> + bad_request + end. + + +update_table() -> + Fields = record_info(fields, passwd), + case mnesia:table_info(passwd, attributes) of + Fields -> + ok; + [user, stored_key] -> + ?INFO_MSG("Converting passwd table from " + "{user, stored_key} format", []), + Host = ?MYNAME, + {atomic, ok} = mnesia:create_table( + ejabberd_auth_internal_tmp_table, + [{disc_only_copies, [node()]}, + {type, bag}, + {local_content, true}, + {record_name, passwd}, + {attributes, record_info(fields, passwd)}]), + mnesia:transform_table(passwd, ignore, Fields), + F1 = fun() -> + mnesia:write_lock_table(ejabberd_auth_internal_tmp_table), + mnesia:foldl( + fun(#passwd{us = U} = R, _) -> + mnesia:dirty_write( + ejabberd_auth_internal_tmp_table, + R#passwd{us = {U, Host}}) + end, ok, passwd) + end, + mnesia:transaction(F1), + mnesia:clear_table(passwd), + F2 = fun() -> + mnesia:write_lock_table(passwd), + mnesia:foldl( + fun(R, _) -> + mnesia:dirty_write(R) + end, ok, ejabberd_auth_internal_tmp_table) + end, + mnesia:transaction(F2), + mnesia:delete_table(ejabberd_auth_internal_tmp_table); + _ -> + ?INFO_MSG("Recreating passwd table", []), + mnesia:transform_table(passwd, ignore, Fields) + end. + + + diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 88486f175..da928599b 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -54,6 +54,7 @@ is_user_exists/2, remove_user/2, remove_user/3, + storage_type/0, plain_password_required/0 ]). @@ -137,6 +138,9 @@ init(Host) -> plain_password_required() -> true. +storage_type() -> + external. + check_password(User, Server, Password) -> %% In LDAP spec: empty password means anonymous authentication. %% As ejabberd is providing other anonymous authentication mechanisms diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl index 0c4eb65da..60e6b8059 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_odbc.erl @@ -43,6 +43,7 @@ is_user_exists/2, remove_user/2, remove_user/3, + storage_type/0, plain_password_required/0 ]). @@ -57,6 +58,9 @@ start(_Host) -> plain_password_required() -> false. +storage_type() -> + plain. + %% @spec (User, Server, Password) -> true | false | {error, Error} check_password(User, Server, Password) -> case jlib:nodeprep(User) of diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index 974985f33..a00d9e816 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -39,6 +39,7 @@ is_user_exists/2, remove_user/2, remove_user/3, + storage_type/0, plain_password_required/0 ]). @@ -106,6 +107,9 @@ remove_user(_User, _Server, _Password) -> plain_password_required() -> true. +storage_type() -> + external. + %%==================================================================== %% Internal functions %%==================================================================== diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 67fee6404..851c818b5 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -772,6 +772,24 @@ wait_for_sasl_response({xmlstreamelement, El}, StateData) -> authenticated = true, auth_module = AuthModule, user = U}); + {ok, Props, ServerOut} -> + (StateData#state.sockmod):reset_stream( + StateData#state.socket), + send_element(StateData, + {xmlelement, "success", + [{"xmlns", ?NS_SASL}], + [{xmlcdata, + jlib:encode_base64(ServerOut)}]}), + U = xml:get_attr_s(username, Props), + AuthModule = xml:get_attr_s(auth_module, Props), + ?INFO_MSG("(~w) Accepted authentication for ~s by ~p", + [StateData#state.socket, U, AuthModule]), + fsm_next_state(wait_for_stream, + StateData#state{ + streamid = new_id(), + authenticated = true, + auth_module = AuthModule, + user = U}); {continue, ServerOut, NewSASLState} -> send_element(StateData, {xmlelement, "challenge", diff --git a/src/scram.erl b/src/scram.erl new file mode 100644 index 000000000..72df11907 --- /dev/null +++ b/src/scram.erl @@ -0,0 +1,55 @@ +-module(scram). +-author('stephen.roettger@googlemail.com'). + +%% External exports +-export([salted_password/3, + stored_key/1, + server_key/1, + server_signature/2, + client_signature/2, + client_key/1, + client_key/2 + ]). + +salted_password(Password, Salt, IterationCount) -> + hi(jlib:nameprep(Password), Salt, IterationCount). + +client_key(SaltedPassword) -> + crypto:sha_mac(SaltedPassword, "Client Key"). + +stored_key(ClientKey) -> + crypto:sha(ClientKey). + +server_key(SaltedPassword) -> + crypto:sha_mac(SaltedPassword, "Server Key"). + +client_signature(StoredKey, AuthMessage) -> + crypto:sha_mac(StoredKey, AuthMessage). + +client_key(ClientProof, ClientSignature) -> + binary:list_to_bin(lists:zipwith(fun(X, Y) -> + X bxor Y + end, + binary:bin_to_list(ClientProof), + binary:bin_to_list(ClientSignature))). + +server_signature(ServerKey, AuthMessage) -> + crypto:sha_mac(ServerKey, AuthMessage). + +hi(Password, Salt, IterationCount) -> + U1 = crypto:sha_mac(Password, string:concat(binary:bin_to_list(Salt), [0,0,0,1])), + binary:list_to_bin(lists:zipwith(fun(X, Y) -> + X bxor Y + end, + binary:bin_to_list(U1), + binary:bin_to_list(hi_round(Password, U1, IterationCount-1)))). + +hi_round(Password, UPrev, 1) -> + crypto:sha_mac(Password, UPrev); +hi_round(Password, UPrev, IterationCount) -> + U = crypto:sha_mac(Password, UPrev), + binary:list_to_bin(lists:zipwith(fun(X, Y) -> + X bxor Y + end, + binary:bin_to_list(U), + binary:bin_to_list(hi_round(Password, U, IterationCount-1)))).