aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBadlop <badlop@process-one.net>2011-08-16 00:25:03 +0200
committerBadlop <badlop@process-one.net>2011-08-16 00:25:03 +0200
commite80b92b48148505b44c6a378db36badfe60fce79 (patch)
treef98bd3590dacac66988fb806c06d1147b49aec57 /src
parenthandle case when roster version on DB is enabled but version is not found. (diff)
Preliminary patch for SASL SCRAM-SHA-1 (thanks to Stephen Röttger)(EJAB-1196)
Diffstat (limited to 'src')
-rw-r--r--src/cyrsasl.erl30
-rw-r--r--src/cyrsasl_anonymous.erl2
-rw-r--r--src/cyrsasl_digest.erl2
-rw-r--r--src/cyrsasl_plain.erl2
-rw-r--r--src/cyrsasl_scram.erl168
-rw-r--r--src/ejabberd.app1
-rw-r--r--src/ejabberd_auth.erl16
-rw-r--r--src/ejabberd_auth_anonymous.erl4
-rw-r--r--src/ejabberd_auth_external.erl4
-rw-r--r--src/ejabberd_auth_internal.erl4
-rw-r--r--src/ejabberd_auth_internal_scram.erl369
-rw-r--r--src/ejabberd_auth_ldap.erl4
-rw-r--r--src/ejabberd_auth_odbc.erl4
-rw-r--r--src/ejabberd_auth_pam.erl4
-rw-r--r--src/ejabberd_c2s.erl18
-rw-r--r--src/scram.erl55
16 files changed, 673 insertions, 14 deletions
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)))).