]> granicus.if.org Git - ejabberd/commitdiff
Preliminary patch for SASL SCRAM-SHA-1 (thanks to Stephen Röttger)(EJAB-1196)
authorBadlop <badlop@process-one.net>
Mon, 15 Aug 2011 22:25:03 +0000 (00:25 +0200)
committerBadlop <badlop@process-one.net>
Mon, 15 Aug 2011 22:25:03 +0000 (00:25 +0200)
16 files changed:
src/cyrsasl.erl
src/cyrsasl_anonymous.erl
src/cyrsasl_digest.erl
src/cyrsasl_plain.erl
src/cyrsasl_scram.erl [new file with mode: 0644]
src/ejabberd.app
src/ejabberd_auth.erl
src/ejabberd_auth_anonymous.erl
src/ejabberd_auth_external.erl
src/ejabberd_auth_internal.erl
src/ejabberd_auth_internal_scram.erl [new file with mode: 0644]
src/ejabberd_auth_ldap.erl
src/ejabberd_auth_odbc.erl
src/ejabberd_auth_pam.erl
src/ejabberd_c2s.erl
src/scram.erl [new file with mode: 0644]

index fd54ce44f1842016275a956229b681139f5e36c1..627ee4d32f582a2c1da5a32493aba2f9d0fe1bb3 100644 (file)
@@ -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}};
index 3af9991d1bb85a9d12c5a485ea212600755de025..1a61d91764d8ce9d0be31e7920fef7f6ef28c8c3 100644 (file)
@@ -34,7 +34,7 @@
 -record(state, {server}).
 
 start(_Opts) ->
-    cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, false),
+    cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, plain),
     ok.
 
 stop() ->
index 40e7edfd87a9cbb3f8e038d1c57f53ff5f5ec07b..eea64cb48d6a356c476a9f6064093ccfdb6ebab8 100644 (file)
@@ -40,7 +40,7 @@
                host}).
 
 start(_Opts) ->
-    cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, true).
+    cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, digest).
 
 stop() ->
     ok.
index 1b7108e31e1f265ad4d63262dcb270938651200e..a8913d3a379d3bf8546847b05cfd9923482b7ff7 100644 (file)
@@ -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 (file)
index 0000000..8b6e6a9
--- /dev/null
@@ -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.
+
index 5ab3b5308492741a6f0d903d4dd7bad35754bd68..031fdaf46d5bd7f7e2b0f8221b79b3443ea959b6 100644 (file)
@@ -10,6 +10,7 @@
             cyrsasl,
             cyrsasl_digest,
             cyrsasl_plain,
+            cyrsasl_scram,
             ejabberd_admin,
             ejabberd_app,
             ejabberd_auth_anonymous,
index 23f9c4ba2c01a4aff3a9361ae2686c47005b9b5e..d937340f1295b102d2b5af7f6c45b6421ecef9c1 100644 (file)
@@ -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
index 787d96b4672654db28760627370a6b49fa4803de..d3ecffc1b97d6ba107bf40a96eb066950a2db9b3 100644 (file)
@@ -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.
index a426fe958c7d18854463e9ab843b7090200bf96d..b625af84c6967f66d0eb0d8fc1e113ea892a31be 100644 (file)
@@ -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);
index 39554bebc42e2b658b5d2da0107a3be7b22c0d84..847737f29b880216b7e5226c7482f36079509928 100644 (file)
@@ -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 (file)
index 0000000..f39ee4f
--- /dev/null
@@ -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.
+
+
+
index 88486f175472fb4c2f665b29102fb321ca16b9b5..da928599b9636c15b1c28b8bc4e2005b29f1c985 100644 (file)
@@ -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
index 0c4eb65da571d8878c604644d6beee64681c1450..60e6b80591315b6aa66b39a64573354a68c17716 100644 (file)
@@ -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
index 974985f3306610358cb53f3e9c6a6f2289aa377d..a00d9e81662a852b1122e18774cba02916b6d5f1 100644 (file)
@@ -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
 %%====================================================================
index 67fee64043d135dffa342c55b4f12a85a3fcab52..851c818b5c7261afb4cb7c2aca0288d43446fa72 100644 (file)
@@ -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 (file)
index 0000000..72df119
--- /dev/null
@@ -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)))).