Password Hashes (no Plain Passwords) in MySQL

There is patch for 2.1.12 version which can store password hashes instead plain passwords in mysql 'users' table.
This is optional. Additional option {password_type,hashed} should be set to enable this feature.
Also I added second option {auth_mechanisms, [digest]}. Which just starts only particular auth backend. Just I want to have exactly defined mechanisms. So for my purpose I use {auth_mechanisms, [digest,plain]}.

Note, mysql.sql schema is changed a bit. Thus to use patch on existent system ou need to alter 'users' table as it is in scheme.

Here is patch

Index: src/ejabberd_auth_odbc.erl
===================================================================
--- src/ejabberd_auth_odbc.erl (.../vendor/2.1.12) (revision 13)
+++ src/ejabberd_auth_odbc.erl (.../trunk) (revision 13)
@@ -70,11 +70,11 @@
    Username = ejabberd_odbc:escape(LUser),
    LServer = jlib:nameprep(Server),
    try odbc_queries:get_password(LServer, Username) of
- {selected, ["password"], [{Password}]} ->
+ {selected, ["password","ha1"], [{Password,_Ha1}]} ->
    Password /= ""; %% Password is correct, and not empty
- {selected, ["password"], [{_Password2}]} ->
+ {selected, ["password","ha1"], [{_Password2,_Ha1}]} ->
    false; %% Password is not correct
- {selected, ["password"], []} ->
+ {selected, ["password","ha1"], []} ->
    false; %% Account does not exist
{error, _Error} ->
    false %% Typical error is that table doesn't exist
@@ -94,7 +94,7 @@
    LServer = jlib:nameprep(Server),
    try odbc_queries:get_password(LServer, Username) of
%% Account exists, check if password is valid
- {selected, ["password"], [{Passwd}]} ->
+ {selected, ["password","ha1"], [{Passwd,_Ha1}]} ->
    DigRes = if
Digest /= "" ->
     Digest == DigestGen(Passwd);
@@ -106,7 +106,7 @@
       true ->
    (Passwd == Password) and (Password /= "")
    end;
- {selected, ["password"], []} ->
+ {selected, ["password","ha1"], []} ->
    false; %% Account does not exist
{error, _Error} ->
    false %% Typical error is that table doesn't exist
@@ -200,11 +200,19 @@
LUser ->
    Username = ejabberd_odbc:escape(LUser),
    LServer = jlib:nameprep(Server),
-     case catch odbc_queries:get_password(LServer, Username) of
- {selected, ["password"], [{Password}]} ->
-     Password;
+     {P,H} = case catch odbc_queries:get_password(LServer, Username) of
+ {selected, ["password","ha1"], [{Password,Ha1}]} ->
+     {Password,Ha1};
_ ->
-     false
+     {null,null}
+     end,
+     case {P,H} of
+ {null,null} ->
+ false;
+ {_,null} ->
+ P;
+ _ ->
+ {P,H}
    end
     end.

@@ -215,11 +223,19 @@
LUser ->
    Username = ejabberd_odbc:escape(LUser),
    LServer = jlib:nameprep(Server),
-     case catch odbc_queries:get_password(LServer, Username) of
- {selected, ["password"], [{Password}]} ->
-     Password;
+     {P,H} = case catch odbc_queries:get_password(LServer, Username) of
+ {selected, ["password", "ha1"], [{Password,Ha1}]} ->
+     {Password,Ha1};
_ ->
-     ""
+     {null,null}
+     end,
+     case {P,H} of
+ {null,null} ->
+ "";
+ {_,null} ->
+ P;
+ _ ->
+ {P,H}
    end
     end.

@@ -232,9 +248,9 @@
    Username = ejabberd_odbc:escape(LUser),
    LServer = jlib:nameprep(Server),
    try odbc_queries:get_password(LServer, Username) of
- {selected, ["password"], [{_Password}]} ->
+ {selected, ["password","ha1"], [{_Password,_Ha1}]} ->
    true; %% Account exists
- {selected, ["password"], []} ->
+ {selected, ["password","ha1"], []} ->
    false; %% Account does not exist
{error, Error} ->
    {error, Error} %% Typical error is that table doesn't exist
Index: src/cyrsasl_digest.erl
===================================================================
--- src/cyrsasl_digest.erl (.../vendor/2.1.12) (revision 13)
+++ src/cyrsasl_digest.erl (.../trunk) (revision 13)
@@ -78,15 +78,37 @@
    case (State#state.get_password)(UserName) of
{false, _} ->
    {error, "not-authorized", UserName};
+ {{Passwd, Ha1}, AuthModule} ->
+     ?DEBUG("Funney-Hash ~p, ~p, ~p",[UserName,Passwd,Ha1]),
+    case (State#state.check_password)(UserName, "",
+ xml:get_attr_s("response", KeyVals),
+ fun(PW) -> response(KeyVals, UserName, PW, Nonce, AuthzId,
+ "AUTHENTICATE",Ha1) end) of
+     {true, _} ->
+
+     RspAuth = response(KeyVals,
+        UserName, Passwd,
+        Nonce, AuthzId, "", Ha1),
+     {continue,
+      "rspauth=" ++ RspAuth,
+      State#state{step = 5,
+ auth_module = AuthModule,
+ username = UserName,
+ authzid = AuthzId}};
+      false ->
+     {error, "not-authorized", UserName};
+      {false, _} ->
+     {error, "not-authorized", UserName}
+     end;
{Passwd, AuthModule} ->
case (State#state.check_password)(UserName, "",
xml:get_attr_s("response", KeyVals),
fun(PW) -> response(KeyVals, UserName, PW, Nonce, AuthzId,
- "AUTHENTICATE") end) of
+ "AUTHENTICATE","") end) of
{true, _} ->
    RspAuth = response(KeyVals,
       UserName, Passwd,
-        Nonce, AuthzId, ""),
+        Nonce, AuthzId, "", ""),
    {continue,
     "rspauth=" ++ RspAuth,
     State#state{step = 5,
@@ -214,7 +236,7 @@
     digit_to_xchar(N div 16) | Res]).

-response(KeyVals, User, Passwd, Nonce, AuthzId, A2Prefix) ->
+response(KeyVals, User, Passwd, Nonce, AuthzId, A2Prefix, Ha1) ->
     Realm = xml:get_attr_s("realm", KeyVals),
     CNonce = xml:get_attr_s("cnonce", KeyVals),
     DigestURI = xml:get_attr_s("digest-uri", KeyVals),
@@ -222,13 +244,27 @@
     QOP = xml:get_attr_s("qop", KeyVals),
     A1 = case AuthzId of
     "" ->
- binary_to_list(
-    crypto:md5(User ++ ":" ++ Realm ++ ":" ++ Passwd)) ++
-      ":" ++ Nonce ++ ":" ++ CNonce;
+ case Ha1 of
+ "" ->
+ binary_to_list(
+    crypto:md5(User ++ ":" ++ Realm ++ ":" ++ Passwd)) ++
+      ":" ++ Nonce ++ ":" ++ CNonce;
+ _ ->
+ binary_to_list(
+    hex:hexstr_to_bin(Ha1)) ++
+      ":" ++ Nonce ++ ":" ++ CNonce
+ end;
     _ ->
- binary_to_list(
-    crypto:md5(User ++ ":" ++ Realm ++ ":" ++ Passwd)) ++
-      ":" ++ Nonce ++ ":" ++ CNonce ++ ":" ++ AuthzId
+ case Ha1 of
+ "" ->
+ binary_to_list(
+    crypto:md5(User ++ ":" ++ Realm ++ ":" ++ Passwd)) ++
+      ":" ++ Nonce ++ ":" ++ CNonce ++ ":" ++ AuthzId;
+ _ ->
+ binary_to_list(
+    hex:hexstr_to_bin(Ha1)) ++
+      ":" ++ Nonce ++ ":" ++ CNonce ++ ":" ++ AuthzId
+ end
end,
     A2 = case QOP of
     "auth" ->
Index: src/cyrsasl.erl
===================================================================
--- src/cyrsasl.erl (.../vendor/2.1.12) (revision 13)
+++ src/cyrsasl.erl (.../trunk) (revision 13)
@@ -52,12 +52,50 @@
     ets:new(sasl_mechanism, [named_table,
     public,
     {keypos, #sasl_mechanism.mechanism}]),
+   
+    Mechs = ejabberd_config:get_local_option(auth_mechanisms),
+    case Mechs of
+        undefined ->
+            start_all_mechs();
+        _ ->
+            case is_atom(Mechs) of
+                true ->
+                    start_all_mechs();
+                false ->
+                    case lists:flatlength(Mechs) of
+                        0 ->
+                            start_all_mechs();
+                        _ ->
+                            start_mechs(Mechs)
+                    end
+            end
+    end,
+    ok.
+
+start_all_mechs() ->
     cyrsasl_plain:start([]),
     cyrsasl_digest:start([]),
     cyrsasl_scram:start([]),
-    cyrsasl_anonymous:start([]),
-    ok.
+    cyrsasl_anonymous:start([]).

+start_mechs([M | Mechs]) ->
+    ?DEBUG("Starting: ~p",[M]),
+    case M of
+        digest ->
+            cyrsasl_digest:start([]);
+        plain ->
+            cyrsasl_plain:start([]);
+        scram ->
+            cyrsasl_scram:start([]);
+        anon ->
+            cyrsasl_anonymous:start([])
+    end,
+    start_mechs(Mechs);
+
+start_mechs([]) ->
+ [].
+
+
register_mechanism(Mechanism, Module, PasswordType) ->
     ets:insert(sasl_mechanism,
       #sasl_mechanism{mechanism = Mechanism,
Index: src/odbc/mysql.sql
===================================================================
--- src/odbc/mysql.sql (.../vendor/2.1.12) (revision 13)
+++ src/odbc/mysql.sql (.../trunk) (revision 13)
@@ -23,6 +23,7 @@
CREATE TABLE users (
     username varchar(250) PRIMARY KEY,
     password text NOT NULL,
+    ha1 text,
     created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) CHARACTER SET utf8;

Index: src/odbc/odbc_queries.erl
===================================================================
--- src/odbc/odbc_queries.erl (.../vendor/2.1.12) (revision 13)
+++ src/odbc/odbc_queries.erl (.../trunk) (revision 13)
@@ -167,23 +167,46 @@
get_password(LServer, Username) ->
     ejabberd_odbc:sql_query(
       LServer,
-      ["select password from users "
+      ["select password, ha1 from users "
        "where username='", Username, "';"]).

set_password_t(LServer, Username, Pass) ->
-    ejabberd_odbc:sql_transaction(
-      LServer,
-      fun() ->
-       update_t("users", ["username", "password"],
-        [Username, Pass],
-        ["username='", Username ,"'"])
-      end).
+    PS = ejabberd_config:get_local_option(password_type),
+    case PS of
+        hashed ->
+         Ha1 = hex:bin_to_hexstr(crypto:md5(Username ++ ":" ++ LServer ++ ":" ++ Pass)),
+         ejabberd_odbc:sql_transaction(
+         LServer,
+         fun() ->
+         update_t("users", ["username", "ha1"],
+        [Username, Ha1],
+        ["username='", Username ,"'"])
+         end);
+        _ ->
+     ejabberd_odbc:sql_transaction(
+         LServer,
+         fun() ->
+         update_t("users", ["username", "password"],
+        [Username, Pass],
+        ["username='", Username ,"'"])
+         end)
+    end.

add_user(LServer, Username, Pass) ->
-    ejabberd_odbc:sql_query(
-      LServer,
-      ["insert into users(username, password) "
-       "values ('", Username, "', '", Pass, "');"]).
+    PS = ejabberd_config:get_local_option(password_type),
+    case PS of
+        hashed ->
+            Ha1 = hex:bin_to_hexstr(crypto:md5(Username ++ ":" ++ LServer ++ ":" ++ Pass)),
+                ejabberd_odbc:sql_query(
+                LServer,
+                 ["insert into users(username, ha1) "
+                 "values ('", Username, "', '", Ha1, "');"]);
+        _ ->
+            ejabberd_odbc:sql_query(
+            LServer,
+             ["insert into users(username, password) "
+             "values ('", Username, "', '", Pass, "');"])
+    end.

del_user(LServer, Username) ->
     ejabberd_odbc:sql_query(
@@ -191,12 +214,24 @@
       ["delete from users where username='", Username ,"';"]).

del_user_return_password(_LServer, Username, Pass) ->
-    P = ejabberd_odbc:sql_query_t(
-   ["select password from users where username='",
-    Username, "';"]),
-    ejabberd_odbc:sql_query_t(["delete from users "
-        "where username='", Username,
-        "' and password='", Pass, "';"]),
+    PS = ejabberd_config:get_local_option(password_type),
+    P = case PS of
+        hashed ->
+            Ha1 = hex:bin_to_hexstr(crypto:md5(Username ++ ":" ++ _LServer ++ ":" ++ Pass)),
+            ejabberd_odbc:sql_query_t(
+             ["select ha1 from users where username='",
+             Username, "';"]),
+            ejabberd_odbc:sql_query_t(["delete from users "
+         "where username='", Username,
+     "' and ha1='", Ha1, "';"]);
+        _ ->
+            ejabberd_odbc:sql_query_t(
+             ["select password from users where username='",
+             Username, "';"]),
+            ejabberd_odbc:sql_query_t(["delete from users "
+         "where username='", Username,
+     "' and password='", Pass, "';"])
+        end,
     P.

list_users(LServer) ->
Index: src/ejabberd.cfg.example
===================================================================
--- src/ejabberd.cfg.example (.../vendor/2.1.12) (revision 13)
+++ src/ejabberd.cfg.example (.../trunk) (revision 13)
@@ -90,6 +90,9 @@
%%
{hosts, ["localhost"]}.

+%%{auth_mechanisms,[digest,plain]}.
+%%{password_type,hashed}.
+
%%
%% route_subdomains: Delegate subdomains to other XMPP servers.
%% For example, if this ejabberd serves example.org and you want
Index: src/ejabberd_config.erl
===================================================================
--- src/ejabberd_config.erl (.../vendor/2.1.12) (revision 13)
+++ src/ejabberd_config.erl (.../trunk) (revision 13)
@@ -443,6 +443,10 @@
    State;
{max_fsm_queue, N} ->
    add_option(max_fsm_queue, N, State);
+ {password_type, PasswordType} ->
+     add_option(password_type, PasswordType, State);
+ {auth_mechanisms, AuthMechs} ->
+     add_option(auth_mechanisms, AuthMechs, State);
{_Opt, _Val} ->
    lists:foldl(fun(Host, S) -> process_host_term(Term, Host, S) end,
State, State#state.hosts)
Index: src/hex.erl
===================================================================
--- src/hex.erl (.../vendor/2.1.12) (revision 0)
+++ src/hex.erl (.../trunk) (revision 13)
@@ -0,0 +1,33 @@
+-module(hex).
+-export([bin_to_hexstr/1,hexstr_to_bin/1]).
+
+hex(N) when N < 10 ->
+    $0+N;
+hex(N) when N >= 10, N < 16 ->
+    $a+(N-10).
+
+int(C) when $0 =< C, C =< $9 ->
+    C - $0;
+int(C) when $A =< C, C =< $F ->
+    C - $A + 10;
+int(C) when $a =< C, C =< $f ->
+    C - $a + 10.
+   
+to_hex(N) when N < 256 ->
+    [hex(N div 16), hex(N rem 16)].
+
+list_to_hexstr([]) ->
+    [];
+list_to_hexstr([H|T]) ->
+    to_hex(H) ++ list_to_hexstr(T).
+
+bin_to_hexstr(Bin) ->
+    list_to_hexstr(binary_to_list(Bin)).
+
+hexstr_to_bin(S) ->
+    list_to_binary(hexstr_to_list(S)).
+
+hexstr_to_list([X,Y|T]) ->
+    [int(X)*16 + int(Y) | hexstr_to_list(T)];
+hexstr_to_list([]) ->
+    [].

Syndicate content