]> granicus.if.org Git - ejabberd/commitdiff
New SASL authentication method: SCRAM-SHA-1 (thanks to Stephen Röttger)(EJAB-1196)
authorBadlop <badlop@process-one.net>
Mon, 15 Aug 2011 22:28:25 +0000 (00:28 +0200)
committerBadlop <badlop@process-one.net>
Mon, 15 Aug 2011 22:28:25 +0000 (00:28 +0200)
18 files changed:
doc/guide.tex
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.cfg.example
src/ejabberd.hrl
src/ejabberd_auth.erl
src/ejabberd_auth_anonymous.erl
src/ejabberd_auth_external.erl
src/ejabberd_auth_ldap.erl
src/ejabberd_auth_pam.erl
src/ejabberd_auth_storage.erl
src/ejabberd_c2s.erl
src/ejabberd_piefxis.erl
src/scram.erl [new file with mode: 0644]

index 35636dcf3490cdd22efbde2a2cdf1facae6a0d5e..890b1bff735aea95d60c167ddcf40b150d2fb73e 100644 (file)
@@ -1225,12 +1225,31 @@ When the storage is configured for ODBC, the ODBC server is
 configured with the \term{odbc\_server} option, see
 \ref{mysql} for MySQL, \ref{pgsql} for PostgreSQL, \ref{mssql} for MSSQL, and \ref{odbc} for generic ODBC.
 
+The option \term{\{auth\_password\_format, plain|scram\}}
+defines in what format the users passwords are stored:
+\begin{description}
+    \titem{plain}
+    The password is stored as plain text in the database.
+    This is risky because the passwords can be read if your database gets compromised.
+    This is the default value.
+    This format allows clients to authenticate using:
+    the old Jabber Non-SASL (\xepref{0078}), \term{SASL PLAIN},
+    \term{SASL DIGEST-MD5}, and \term{SASL SCRAM-SHA-1}.
+
+    \titem{scram}
+    The password is not stored, only some information that allows to verify the hash provided by the client.
+    It is impossible to obtain the original plain password from the stored information;
+    for this reason, when this value is configured it cannot be changed to \term{plain} anymore.
+    This format allows clients to authenticate using: \term{SASL PLAIN} and \term{SASL SCRAM-SHA-1}.
+\end{description}
+
 Examples:
 \begin{itemize}
-\item To use internal Mnesia storage on all virtual hosts:
+\item To use internal Mnesia storage with hashed passwords on all virtual hosts:
 \begin{verbatim}
 {auth_method, storage}.
 {auth_storage, mnesia}.
+{auth_password_format, scram}.
 \end{verbatim}
 \item To use ODBC storage on all virtual hosts:
 \begin{verbatim}
index 94df55fe0a0ad65f6f6d7270f033582a31ae454b..fcfc2456a1ec2c6073bab92eeca6b458d2645014 100644 (file)
@@ -43,7 +43,7 @@
 %%     Require_Plain = bool().
 %% Registry entry of a supported SASL mechanism.
 
--record(sasl_mechanism, {mechanism, module, require_plain_password}).
+-record(sasl_mechanism, {mechanism, module, password_type}).
 
 %% @type saslstate() = {sasl_state, Service, Myname, Realm, GetPassword, CheckPassword, CheckPasswordDigest, Mech_Mod, Mech_State}
 %%     Service = string()
@@ -76,6 +76,7 @@ start() ->
                             {keypos, #sasl_mechanism.mechanism}]),
     cyrsasl_plain:start([]),
     cyrsasl_digest:start([]),
+    cyrsasl_scram:start([]),
     cyrsasl_anonymous:start([]),
     maybe_try_start_gssapi(),
     ok.
@@ -101,11 +102,11 @@ try_start_gssapi() ->
 %%     Module = atom()
 %%     Require_Plain = bool()
 
-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").
@@ -153,17 +154,20 @@ check_credentials(_State, Props) ->
 %%     Mechanism = string()
 
 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 catch ejabberd_auth:store_type(Host) of
+                        external ->
+                             [{'==', '$2', plain}];
+                        scram ->
+                             [{'/=', '$2', digest}];
+                        {'EXIT',{undef,[{Module,store_type,[]} | _]}} ->
+                             ?WARNING_MSG("~p doesn't implement the function store_type/0", [Module]),
+                             [];
+                        _Else ->
+                             []
                         end,
                         ['$1']}]),
     filter_anonymous(Host, Mechs).
@@ -252,6 +256,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 555ace8924f6cbab08d8c851e1c87151b2225266..e65cf2d3104baab0b25b06ad44d9884d50102c0d 100644 (file)
@@ -42,7 +42,7 @@
 %%     Opts = term()
 
 start(_Opts) ->
-    cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, false),
+    cyrsasl:register_mechanism("ANONYMOUS", ?MODULE, plain),
     ok.
 
 %% @spec () -> ok
index 92658f5541b44af830b1a668d5ac9f2acb821add..e8f0488f2de33c94970f1a6265ac842d8a664037 100644 (file)
@@ -53,7 +53,7 @@
 %%     Opts = term()
 
 start(_Opts) ->
-    cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, true).
+    cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE, digest).
 
 %% @spec () -> ok
 
index 7b529d21086d568dbd1a141c58098d90a61f05e0..4d5176dc08d97a22078c4f1a8f82838f7297f912 100644 (file)
@@ -42,7 +42,7 @@
 %%     Opts = term()
 
 start(_Opts) ->
-    cyrsasl:register_mechanism("PLAIN", ?MODULE, false),
+    cyrsasl:register_mechanism("PLAIN", ?MODULE, plain),
     ok.
 
 %% @spec () -> ok
diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl
new file mode 100644 (file)
index 0000000..42f33d7
--- /dev/null
@@ -0,0 +1,197 @@
+%%%----------------------------------------------------------------------
+%%% File    : cyrsasl_scram.erl
+%%% Author  : Stephen Röttger <stephen.roettger@googlemail.com>
+%%% Purpose : SASL SCRAM authentication
+%%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2011   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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-module(cyrsasl_scram).
+-author('stephen.roettger@googlemail.com').
+
+-export([start/1,
+        stop/0,
+        mech_new/1,
+        mech_step/2]).
+
+-include("ejabberd.hrl").
+-include("cyrsasl.hrl").
+
+-behaviour(cyrsasl).
+
+-record(state, {step, stored_key, server_key, username, get_password, check_password,
+               auth_message, client_nonce, server_nonce}).
+
+-define(SALT_LENGTH, 16).
+-define(NONCE_LENGTH, 16).
+
+start(_Opts) ->
+    cyrsasl:register_mechanism("SCRAM-SHA-1", ?MODULE, scram).
+
+stop() ->
+    ok.
+
+mech_new(#sasl_params{get_password=GetPassword}) ->
+    {ok, #state{step = 2, get_password = GetPassword}}.
+
+mech_step(#state{step = 2} = State, ClientIn) ->
+       case string:tokens(ClientIn, ",") of
+       [CBind, UserNameAttribute, ClientNonceAttribute] when (CBind == "y") or (CBind == "n") ->
+               case parse_attribute(UserNameAttribute) of
+               {error, Reason} ->
+                       {error, Reason};
+               {_, 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, ?SCRAM_DEFAULT_ITERATION_COUNT),
+                                                       {scram:stored_key(scram:client_key(SaltedPassword)),
+                                                        scram:server_key(SaltedPassword), TempSalt, ?SCRAM_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;
+               _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, CVal} when (CVal == "biws") or (CVal == "eSws") ->
+                   %% biws is base64 for n,, => channelbinding not supported
+                   %% eSws is base64 for y,, => channelbinding supported by client only
+                       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 68e648919f998b4505252637757217efcf9f2b97..0b524537efbbcff7629c888ca9dcca34872af93b 100644 (file)
@@ -8,6 +8,7 @@
             cyrsasl,
             cyrsasl_digest,
             cyrsasl_plain,
+            cyrsasl_scram,
             ejabberd_admin,
             ejabberd_app,
             ejabberd_auth_anonymous,
index 9f2415f63d5125bfb823308cd20806641a41b180..d727bf71697c70713c744fb6a5c2d60244031233 100644 (file)
 %%
 %%{host_config, "public.example.org", [{auth_method, [internal, anonymous]}]}.
 
+%%
+%% auth_password_format: Format of storing users passwords
+%% The default format is plain text.
+%% If you change to hashed scram, you can never go back to plain.
+%% This option is only supported by the 'storage' auth_method.
+%% 
+{auth_password_format, plain}.
+%%{auth_password_format, scram}.
+
 
 %%%.   ==============
 %%%'   DATABASE SETUP
index 8c07db530c33612de19bf8df51311e52eecf35ab..3c52b04f093a8e0a14821692e9c5f08e4706c2a7 100644 (file)
@@ -47,6 +47,9 @@
 
 %%-define(DBGFSM, true).
 
+-record(scram, {storedkey, serverkey, salt, iterationcount}).
+-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096).
+
 %% ---------------------------------
 %% Logging mechanism
 
index 38876302e7cc94466aac27aba164342d7f5f358c..344232c8688c04dbedd2cd81cf156c1107eca572 100644 (file)
@@ -50,6 +50,7 @@
         remove_user/2,
         remove_user/3,
         plain_password_required/1,
+        store_type/1,
         entropy/1
        ]).
 
@@ -105,12 +106,31 @@ stop_methods(Host, Method) when is_atom(Method) ->
 %% @spec (Server) -> bool()
 %%     Server = string()
 
+%% This is only executed by ejabberd_c2s for non-SASL auth client
 plain_password_required(Server) when is_list(Server) ->
     lists:any(
       fun(M) ->
              M:plain_password_required()
       end, auth_modules(Server)).
 
+%% @spec (Server) -> bool()
+%%     Server = string()
+
+store_type(Server) ->
+    lists:foldl(
+      fun(_, external) ->
+             external;
+         (M, scram) ->
+             case M:store_type() of
+                 external ->
+                     external;
+                 _Else ->
+                     scram
+                 end;
+         (M, plain) ->
+             M:store_type()
+      end, plain, auth_modules(Server)).
+
 %% @spec (User, Server, Password) -> bool()
 %%     User = string()
 %%     Server = string()
@@ -342,8 +362,10 @@ get_password_s(User, Server) when is_list(User), is_list(Server) ->
     case get_password(User, Server) of
        false ->
            "";
-       Password ->
-           Password
+       Password when is_list(Password) ->
+           Password;
+       _ ->
+           ""
     end.
 
 %% @spec (User, Server) -> {Password, AuthModule} | {false, none}
index 1ea1ec6726991466f7c7e91f0122cd90f527ef1b..2dcb597d50a193b2dd57abe6820d13742c417ed3 100644 (file)
@@ -52,6 +52,7 @@
         is_user_exists/2,
         remove_user/2,
         remove_user/3,
+        store_type/0,
         plain_password_required/0]).
 
 -include_lib("exmpp/include/exmpp.hrl").
@@ -360,6 +361,9 @@ remove_user(_User, _Server, _Password) ->
 plain_password_required() ->
     false.
 
+store_type() ->
+       plain.
+
 update_tables() ->
     case catch mnesia:table_info(anonymous, local_content) of
        false ->
index 9a7af075c3b5e28085e9afc49cbc8400f922c0b6..d5ae7198adca20091d521a4e7ee9441cfc6025bd 100644 (file)
@@ -44,6 +44,7 @@
         is_user_exists/2,
         remove_user/2,
         remove_user/3,
+        store_type/0,
         plain_password_required/0
        ]).
 
@@ -99,6 +100,9 @@ plain_password_required() ->
 %%     Server = string()
 %%     Password = string()
 
+store_type() ->
+       external.
+
 check_password(User, Server, Password) ->
     case get_cache_option(Server) of
        false -> check_password_extauth(User, Server, Password);
index ceac2cb4f3ee64cb0b8024281030c9d6a1e14bc2..61b2fa4077390c0b671a4515821400a442b5b4d3 100644 (file)
@@ -54,6 +54,7 @@
         is_user_exists/2,
         remove_user/2,
         remove_user/3,
+        store_type/0,
         plain_password_required/0
        ]).
 
@@ -184,6 +185,9 @@ plain_password_required() ->
 %%     Server = string()
 %%     Password = string()
 
+store_type() ->
+       external.
+
 check_password(User, Server, Password) ->
     %% In LDAP spec: empty password means anonymous authentication.
     %% As ejabberd is providing other anonymous authentication mechanisms
index 07657c9f16e83038cdf39ea2f95169bab75b1871..97544695e1d24d0232d751f9d612c9f5cac167bb 100644 (file)
@@ -40,6 +40,7 @@
         is_user_exists/2,
         remove_user/2,
         remove_user/3,
+        store_type/0,
         plain_password_required/0
        ]).
 
@@ -171,6 +172,9 @@ remove_user(_User, _Server, _Password) ->
 plain_password_required() ->
     true.
 
+store_type() ->
+       external.
+
 %%====================================================================
 %% Internal functions
 %%====================================================================
index e9fc0b0cbcbca306d835d7e31fa07f0dd0236909..54a4608239033b7a2f94bc8a9f1c279fa7b83962 100644 (file)
 %%%  user_host = {Username::string(), Host::string()}
 %%%  password = string()
 %%%
+%%% 3.0.0-beta / mnesia / passwd
+%%%  user_host = {Username::string(), Host::string()}
+%%%  password = string()
+%%%  storedkey = base64 binary()
+%%%  serverkey = base64 binary()
+%%%  iterationcount = integer()
+%%%  salt = base64 binary()
+%%%
 %%% 3.0.0-alpha / odbc / passwd
 %%%  user = varchar150
 %%%  host = varchar150
 %%%  password = text
+%%%
+%%% 3.0.0-beta / odbc / passwd
+%%%  user = varchar150
+%%%  host = varchar150
+%%%  password = base64 text
+%%%  storedkey = base64 text
+%%%  serverkey = base64 text
+%%%  iterationcount = integer
+%%%  salt = base64 text
 
 -module(ejabberd_auth_storage).
 -author('alexey@process-one.net').
         is_user_exists/2,
         remove_user/2,
         remove_user/3,
+        store_type/0,
         plain_password_required/0
        ]).
 
 -include("ejabberd.hrl").
 
--record(passwd, {user_host, password}).
+-record(passwd, {user_host, password, storedkey, serverkey, salt, iterationcount}).
 -record(reg_users_counter, {vhost, count}).
 
+-define(SALT_LENGTH, 16).
+
 %%%----------------------------------------------------------------------
 %%% API
 %%%----------------------------------------------------------------------
@@ -95,13 +115,19 @@ start(Host) ->
                             [{odbc_host, Host},
                              {disc_copies, [node()]},
                              {attributes, record_info(fields, passwd)},
-                             {types, [{user_host, {text, text}}]}
+                             {types, [{user_host, {text, text}},
+                                      {storedkey, binary},
+                                      {serverkey, binary},
+                                      {salt, binary},
+                                      {iterationcount, int}]}
                             ]),
     update_table(Host, Backend),
+    maybe_scram_passwords(Host),
     mnesia:create_table(reg_users_counter,
                        [{ram_copies, [node()]},
                         {attributes, record_info(fields, reg_users_counter)}]),
     update_reg_users_counter_table(Host),
+    maybe_alert_password_scrammed_without_option(Host),
     ok.
 
 stop(_Host) ->
@@ -120,7 +146,16 @@ update_reg_users_counter_table(Server) ->
 %% @spec () -> bool()
 
 plain_password_required() ->
-    false.
+    case is_scrammed(?MYNAME) of
+       false -> false;
+       true -> true
+    end.
+
+store_type() ->
+    case is_scrammed(?MYNAME) of
+       false -> plain; %% allows: PLAIN DIGEST-MD5 SCRAM
+       true -> scram %% allows: PLAIN SCRAM
+    end.
 
 %% @spec (User, Server, Password) -> bool()
 %%     User = string()
@@ -132,6 +167,8 @@ check_password(User, Server, Password) ->
     LServer = exmpp_stringprep:nameprep(Server),
     US = {LUser, LServer},
     case catch gen_storage:dirty_read(LServer, {passwd, US}) of
+       [#passwd{password = ""} = Passwd] ->
+           is_password_scram_valid(Password, Passwd);
        [#passwd{password = Password}] ->
            Password /= "";
        _ ->
@@ -150,6 +187,19 @@ check_password(User, Server, Password, Digest, DigestGen) ->
     LServer = exmpp_stringprep:nameprep(Server),
     US = {LUser, LServer},
     case catch gen_storage:dirty_read(LServer, {passwd, US}) of
+       [#passwd{password = ""} = Passwd] ->
+           Passwd = base64:decode(Passwd#passwd.storedkey),
+           DigRes = if
+                        Digest /= "" ->
+                            Digest == DigestGen(Passwd);
+                        true ->
+                            false
+                    end,
+           if DigRes ->
+                   true;
+              true ->
+                   (Passwd == Password) and (Password /= "")
+           end;
        [#passwd{password = Passwd}] ->
            DigRes = if
                         Digest /= "" ->
@@ -182,9 +232,11 @@ set_password(User, Server, Password) ->
        US ->
            %% TODO: why is this a transaction?
            F = fun() ->
-                       gen_storage:write(LServer,
-                                         #passwd{user_host = US,
-                                                 password = Password})
+                       Passwd = case is_scrammed(LServer) and (Password /= "") of
+                                       true -> password_to_scram(Password, #passwd{user_host=US});
+                                       false -> #passwd{user_host = US, password = Password}
+                                   end,
+                       gen_storage:write(LServer, Passwd)
                end,
            {atomic, ok} = gen_storage:transaction(LServer, passwd, F),
            ok
@@ -207,9 +259,11 @@ try_register(User, Server, Password) ->
            F = fun() ->
                        case gen_storage:read(LServer, {passwd, US}) of
                            [] ->
-                               gen_storage:write(LServer,
-                                                 #passwd{user_host = US,
-                                                         password = Password}),
+                               Passwd = case is_scrammed(LServer) and (Password /= "") of
+                                               true -> password_to_scram(Password, #passwd{user_host=US});
+                                               false -> #passwd{user_host = US, password = Password}
+                                           end,
+                               gen_storage:write(LServer, Passwd),
                                mnesia:dirty_update_counter(
                                                    reg_users_counter,
                                                    exmpp_jid:prep_domain(exmpp_jid:parse(Server)), 1),
@@ -352,9 +406,14 @@ get_password(User, Server) ->
        LServer = exmpp_stringprep:nameprep(Server),
        US = {LUser, LServer},
         case catch gen_storage:dirty_read(LServer, passwd, US) of
-           [#passwd{password = Password}] ->
+       [#passwd{password = ""} = Passwd] ->
+           {base64:decode(Passwd#passwd.storedkey),
+            base64:decode(Passwd#passwd.serverkey),
+            base64:decode(Passwd#passwd.salt),
+            Passwd#passwd.iterationcount};
+       [#passwd{password = Password}] ->
                Password;
-           _ ->
+       _ ->
                false
        end
     catch
@@ -373,8 +432,8 @@ get_password_s(User, Server) ->
        LServer = exmpp_stringprep:nameprep(Server),
        US = {LUser, LServer},
         case catch gen_storage:dirty_read(LServer, passwd, US) of
-           [#passwd{password = Password}] ->
-               Password;
+       [#passwd{password = Password}] ->
+           Password;
            _ ->
                []
        end
@@ -441,13 +500,21 @@ remove_user(User, Server, Password) ->
        US = {LUser, LServer},
        F = fun() ->
                    case gen_storage:read(LServer, {passwd, US}) of
+                   [#passwd{password = ""} = Passwd] ->
+                       case is_password_scram_valid(Password, Passwd) of
+                           true ->
+                               gen_storage:delete(LServer, {passwd, US}),
+                               mnesia:dirty_update_counter(reg_users_counter,
+                                                           LServer, -1),
+                               ok;
+                           false ->
+                               not_allowed
+                       end;
                        [#passwd{password = Password}] ->
                            gen_storage:delete(LServer, {passwd, US}),
                            mnesia:dirty_update_counter(reg_users_counter,
                                                        exmpp_jid:prep_domain(exmpp_jid:parse(Server)), -1),
                            ok;
-                       [_] ->
-                           not_allowed;
                        _ ->
                            not_exists
                    end
@@ -463,13 +530,120 @@ remove_user(User, Server, Password) ->
            bad_request
     end.
 
+%%%
+%%% SCRAM
+%%%
+
+%% The passwords are stored scrammed in the table either if the option says so,
+%% or if at least the first password is empty.
+is_scrammed(Host) ->
+    case action_password_format(Host) of
+       scram -> true;
+       must_scram -> true;
+       plain -> false;
+       forced_scram -> true
+    end.
+
+action_password_format(Host) ->
+    OptionScram = is_option_scram(),
+    case {OptionScram, get_format_first_element(Host)} of
+       {true, scram} -> scram;
+       {true, any} -> scram;
+       {true, plain} -> must_scram;
+       {false, plain} -> plain;
+       {false, any} -> plain;
+       {false, scram} -> forced_scram
+    end.
+
+get_format_first_element(Host) ->
+    case gen_storage:dirty_select(Host, passwd, []) of
+       [] -> any;
+       [#passwd{password = ""} | _] -> scram;
+       [#passwd{} | _] -> plain
+    end.
+
+is_option_scram() ->
+    scram == ejabberd_config:get_local_option({auth_password_format, ?MYNAME}).
+
+maybe_alert_password_scrammed_without_option(Host) ->
+    case is_scrammed(Host) andalso not is_option_scram() of
+       true ->
+           ?ERROR_MSG("Some passwords were stored in the database as SCRAM, "
+                      "but 'auth_password_format' is not configured 'scram'. "
+                      "The option will now be considered to be 'scram'.", []);
+       false ->
+           ok
+    end.
+
+maybe_scram_passwords(Host) ->
+    case action_password_format(Host) of
+       must_scram -> scram_passwords(Host);
+       _ -> ok
+    end.
+
+scram_passwords(Host) ->
+    Backend =
+        case ejabberd_config:get_local_option({auth_storage, Host}) of
+            undefined -> mnesia;
+            B -> B
+        end,
+    scram_passwords(Host, Backend).
+scram_passwords(Host, mnesia) ->
+    ?INFO_MSG("Converting the passwords stored in odbc for host ~p into SCRAM bits", [Host]),
+    gen_storage_migration:migrate_mnesia(
+      Host, passwd,
+      [{passwd, [user_host, password, storedkey, serverkey, iterationcount, salt],
+       fun(#passwd{password = Password} = Passwd) ->
+               password_to_scram(Password, Passwd)
+       end}]);
+scram_passwords(Host, odbc) ->
+    ?INFO_MSG("Converting the passwords stored in odbc for host ~p into SCRAM bits", [Host]),
+    gen_storage_migration:migrate_odbc(
+      Host, [passwd],
+      [{"passwd", ["user", "host", "password", "storedkey", "serverkey", "iterationcount", "salt"],
+       fun(_, User, Host2, Password, _Storedkey, _Serverkey, _Iterationcount, _Salt) ->
+               password_to_scram(Password, #passwd{user_host = {User, Host2}})
+       end}]).
+
+password_to_scram(Password, Passwd) ->
+    password_to_scram(Password, Passwd, ?SCRAM_DEFAULT_ITERATION_COUNT).
+
+password_to_scram(Password, Passwd, IterationCount) ->
+    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),
+    Passwd#passwd{password = "",
+          storedkey = base64:encode(StoredKey),
+          salt = base64:encode(Salt),
+          iterationcount = IterationCount,
+          serverkey = base64:encode(ServerKey)}.
+
+is_password_scram_valid(Password, Passwd) ->
+    IterationCount = Passwd#passwd.iterationcount,
+    Salt = base64:decode(Passwd#passwd.salt),
+    SaltedPassword = scram:salted_password(Password, Salt, IterationCount),
+    StoredKey = scram:stored_key(scram:client_key(SaltedPassword)),
+    (base64:decode(Passwd#passwd.storedkey) == StoredKey).
+
+
 update_table(Host, mnesia) ->
     gen_storage_migration:migrate_mnesia(
       Host, passwd,
       [{passwd, [us, password],
        fun({passwd, {User, _Host}, Password}) ->
-               #passwd{user_host = {User, Host},
-                       password = Password}
+               case is_list(Password) of
+                   true ->
+                       #passwd{user_host = {User, Host},
+                               password = Password};
+                   false ->
+                       #passwd{user_host = {User, Host},
+                               password = "",
+                               storedkey = Password#scram.storedkey,
+                               serverkey = Password#scram.serverkey,
+                               salt = Password#scram.salt,
+                               iterationcount = Password#scram.iterationcount}
+               end
        end}]);
 update_table(Host, odbc) ->
     gen_storage_migration:migrate_odbc(
index e8234195f3f654c06c81252aa271d1957760db16..7c2872e6536c1057e45cbff90a2dc4e181840aff 100644 (file)
@@ -844,6 +844,21 @@ wait_for_sasl_response({xmlstreamelement, #xmlel{ns = NS, name = Name} = El},
                            StateData#state.socket),
                    send_element(StateData, exmpp_server_sasl:success()),
                    U = proplists:get_value(username, Props),
+                   AuthModule = proplists:get_value(auth_module, Props),
+                   ?INFO_MSG("(~w) Accepted authentication for ~s by ~s",
+                             [StateData#state.socket, U, AuthModule]),
+                   fsm_next_state(wait_for_stream,
+                                  StateData#state{
+                                    streamid = new_id(),
+                                    authenticated = true,
+                                    auth_module = AuthModule,
+                                    user = list_to_binary(U)});
+               {ok, Props, ServerOut} ->
+                   catch (StateData#state.sockmod):reset_stream(
+                     StateData#state.socket),
+                   send_element(StateData, exmpp_server_sasl:success(ServerOut)),
+                   U = proplists:get_value(username, Props),
+
                    AuthModule = proplists:get_value(auth_module, Props),
                    ?INFO_MSG("(~w) Accepted authentication for ~s by ~s",
                              [StateData#state.socket, U, AuthModule]),
index 50a5834b01a6df3930b0967e78a72e3cf297aaab..cea7f7c0d687c442edb4c12ab8aff1f0f5770c60 100644 (file)
@@ -159,21 +159,24 @@ process_element(El,State) ->
 
 add_user(El, Domain) ->
     User = exmpp_xml:get_attribute(El,<<"name">>,none),
+    PasswordFormat = exmpp_xml:get_attribute(El,<<"password-format">>,none),
     Password = exmpp_xml:get_attribute(El,<<"password">>,none),
-    add_user(El, Domain, User, Password).
+    add_user(El, Domain, User, PasswordFormat, Password).
 
-%% @spec (El::xmlel(), Domain::string(), User::binary(), Password::binary() | none)
+%% @spec (El::xmlel(), Domain::string(), User::binary(), PasswordFormat, Password::binary() | none)
 %%       -> ok | {error, ErrorText::string()}
+%% PasswordFormat = <<"plaintext">> | <<"scram">>
 %% @doc Add a new user to the database.
 %% If user already exists, it will be only updated.
-add_user(El, Domain, User, none) ->
+add_user(El, Domain, User, <<"plaintext">>, none) ->
     io:format("Account ~s@~s will not be created, updating it...~n",
              [User, Domain]),
     io:format(""),
     populate_user_with_elements(El, Domain, User),
     ok;
-add_user(El, Domain, User, Password) ->
-    case create_user(User,Password,Domain) of
+add_user(El, Domain, User, PasswordFormat, Password) ->
+    Password2 = prepare_password(PasswordFormat, Password, El),
+    case create_user(User,Password2,Domain) of
        ok ->
            populate_user_with_elements(El, Domain, User),
            ok;
@@ -188,6 +191,21 @@ add_user(El, Domain, User, Password) ->
            {error, Other}
     end.
 
+prepare_password(<<"plaintext">>, PasswordBinary, _El) ->
+    ?BTL(PasswordBinary);
+prepare_password(<<"scram">>, none, El) ->
+    ScramEl = exmpp_xml:get_element(El, 'scram-hash'),
+    #scram{storedkey = base64:decode(exmpp_xml:get_attribute(
+                                       ScramEl, <<"stored-key">>, none)),
+          serverkey = base64:decode(exmpp_xml:get_attribute(
+                                       ScramEl, <<"server-key">>, none)),
+          salt = base64:decode(exmpp_xml:get_attribute(
+                                 ScramEl, <<"salt">>, none)),
+          iterationcount = list_to_integer(exmpp_xml:get_attribute_as_list(
+                                              ScramEl, <<"iteration-count">>,
+                                              ?SCRAM_DEFAULT_ITERATION_COUNT))
+         }.
+
 populate_user_with_elements(El, Domain, User) ->
     exmpp_xml:foreach(
       fun (_,Child) ->
@@ -482,10 +500,23 @@ export_user(Fd, Username, Host) ->
 
 %% @spec (Username::string(), Host::string()) -> string()
 extract_user(Username, Host) ->
-    Password = ejabberd_auth:get_password_s(Username, Host),
+    Password = ejabberd_auth:get_password(Username, Host),
+    PasswordStr = build_password_string(Password),
     UserInfo = [extract_user_info(InfoName, Username, Host) || InfoName <- [roster, offline, private, vcard]],
     UserInfoString = lists:flatten(UserInfo),
-    io_lib:format("<user name='~s' password='~s'>~s</user>", [Username, Password, UserInfoString]).
+    io_lib:format("<user name='~s' ~s ~s</user>",
+                 [Username, PasswordStr, UserInfoString]).
+
+build_password_string({StoredKey, ServerKey, Salt, IterationCount}) ->
+    io_lib:format("password-format='scram'>"
+                 "<scram-hash stored-key='~s' server-key='~s' "
+                 "salt='~s' iteration-count='~w'/> ",
+                 [base64:encode_to_string(StoredKey),
+                  base64:encode_to_string(ServerKey),
+                  base64:encode_to_string(Salt),
+                  IterationCount]);
+build_password_string(Password) when is_list(Password) ->
+    io_lib:format("password-format='plaintext' password='~s'>", [Password]).
 
 %% @spec (InfoName::atom(), Username::string(), Host::string()) -> string()
 extract_user_info(roster, Username, Host) ->
diff --git a/src/scram.erl b/src/scram.erl
new file mode 100644 (file)
index 0000000..30bd6bb
--- /dev/null
@@ -0,0 +1,81 @@
+%%%----------------------------------------------------------------------
+%%% File    : scram.erl
+%%% Author  : Stephen Röttger <stephen.roettger@googlemail.com>
+%%% Purpose : SCRAM (RFC 5802)
+%%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2011   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., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-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)))).