couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From rnew...@apache.org
Subject git commit: COUCHDB-1060 - Switch to PBKDF2 for new passwords
Date Mon, 26 Mar 2012 16:07:23 GMT
Updated Branches:
  refs/heads/master 394a08a1e -> 7d4181346


COUCHDB-1060 - Switch to PBKDF2 for new passwords


Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/7d418134
Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/7d418134
Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/7d418134

Branch: refs/heads/master
Commit: 7d4181346626c0cdb50b44f7e5e33435a8ccae0f
Parents: 394a08a
Author: Robert Newson <rnewson@apache.org>
Authored: Mon Mar 26 16:55:16 2012 +0100
Committer: Robert Newson <rnewson@apache.org>
Committed: Mon Mar 26 16:55:16 2012 +0100

----------------------------------------------------------------------
 CHANGES                          |    5 ++
 NEWS                             |    1 +
 etc/couchdb/default.ini.tpl.in   |    1 +
 src/couchdb/Makefile.am          |    2 +
 src/couchdb/couch_auth_cache.erl |   29 ++++++++---
 src/couchdb/couch_httpd_auth.erl |   28 ++++++----
 src/couchdb/couch_passwords.erl  |   94 +++++++++++++++++++++++++++++++++
 src/couchdb/couch_server.erl     |   11 +++-
 src/couchdb/couch_users_db.erl   |   15 ++++--
 test/etap/230-pbkfd2.t           |   38 +++++++++++++
 10 files changed, 198 insertions(+), 26 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/CHANGES
----------------------------------------------------------------------
diff --git a/CHANGES b/CHANGES
index cdfccb9..833812c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -23,6 +23,11 @@ Futon:
 
  * Added view request duration to Futon.
 
+Security:
+
+  * Passwords are now hashed using the PBKDF2 algorithm with a
+    configurable work factor.
+
 Version 1.2.0
 -------------
 

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/NEWS
----------------------------------------------------------------------
diff --git a/NEWS b/NEWS
index c17349d..ab2ef21 100644
--- a/NEWS
+++ b/NEWS
@@ -17,6 +17,7 @@ This version has not been released yet.
  * Speedup in the communication with external view servers.
  * Fixed unnecessary conflict when deleting and creating a
    document in the same batch.
+ * New and updated passwords are hashed using PBKDF2.
 
 Version 1.2.0
 -------------

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/etc/couchdb/default.ini.tpl.in
----------------------------------------------------------------------
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index cebf242..ce84905 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -65,6 +65,7 @@ require_valid_user = false
 timeout = 600 ; number of seconds before automatic logout
 auth_cache_size = 50 ; size is number of cache entries
 allow_persistent_cookies = false ; set to true to allow persistent cookies
+iterations = 10000 ; iterations for password hashing
 
 [couch_httpd_oauth]
 ; If set to 'true', oauth token and consumer secrets will be looked up

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/src/couchdb/Makefile.am
----------------------------------------------------------------------
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
index 8efb1c0..5705976 100644
--- a/src/couchdb/Makefile.am
+++ b/src/couchdb/Makefile.am
@@ -61,6 +61,7 @@ source_files = \
     couch_native_process.erl \
     couch_os_daemons.erl \
     couch_os_process.erl \
+    couch_passwords.erl \
     couch_primary_sup.erl \
     couch_query_servers.erl \
     couch_ref_counter.erl \
@@ -116,6 +117,7 @@ compiled_files = \
     couch_native_process.beam \
     couch_os_daemons.beam \
     couch_os_process.beam \
+    couch_passwords.beam \
     couch_primary_sup.beam \
     couch_query_servers.beam \
     couch_ref_counter.beam \

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/src/couchdb/couch_auth_cache.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_auth_cache.erl b/src/couchdb/couch_auth_cache.erl
index ff66dfb..6bdf1f4 100644
--- a/src/couchdb/couch_auth_cache.erl
+++ b/src/couchdb/couch_auth_cache.erl
@@ -49,20 +49,35 @@ get_user_creds(UserName) ->
         [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
         case get_from_cache(UserName) of
         nil ->
-            [{<<"roles">>, [<<"_admin">>]},
-                {<<"salt">>, ?l2b(Salt)},
-                {<<"password_sha">>, ?l2b(HashedPwd)}];
+            make_admin_doc(HashedPwd, Salt, [<<"_admin">>]);
         UserProps when is_list(UserProps) ->
-            DocRoles = couch_util:get_value(<<"roles">>, UserProps),
-            [{<<"roles">>, [<<"_admin">> | DocRoles]},
-                {<<"salt">>, ?l2b(Salt)},
-                {<<"password_sha">>, ?l2b(HashedPwd)}]
+            make_admin_doc(HashedPwd, Salt, couch_util:get_value(<<"roles">>,
UserProps))
         end;
+    "-pbkdf2-" ++ HashedPwdSaltAndIterations ->
+        [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
+        case get_from_cache(UserName) of
+        nil ->
+            make_admin_doc(HashedPwd, Salt, Iterations, [<<"_admin">>]);
+        UserProps when is_list(UserProps) ->
+            make_admin_doc(HashedPwd, Salt, Iterations, couch_util:get_value(<<"roles">>,
UserProps))
+    end;
     _Else ->
         get_from_cache(UserName)
     end,
     validate_user_creds(UserCreds).
 
+make_admin_doc(HashedPwd, Salt, Roles) ->
+    [{<<"roles">>, Roles},
+     {<<"salt">>, ?l2b(Salt)},
+     {<<"password_scheme">>, <<"simple">>},
+     {<<"password_sha">>, ?l2b(HashedPwd)}].
+
+make_admin_doc(DerivedKey, Salt, Iterations, Roles) ->
+    [{<<"roles">>, Roles},
+     {<<"salt">>, ?l2b(Salt)},
+     {<<"iterations">>, list_to_integer(Iterations)},
+     {<<"password_scheme">>, <<"pbkdf2">>},
+     {<<"derived_key">>, ?l2b(DerivedKey)}].
 
 get_from_cache(UserName) ->
     exec_if_auth_db(

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/src/couchdb/couch_httpd_auth.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl
index c09823c..0b4ba8f 100644
--- a/src/couchdb/couch_httpd_auth.erl
+++ b/src/couchdb/couch_httpd_auth.erl
@@ -69,10 +69,7 @@ default_authentication_handler(Req) ->
             nil ->
                 throw({unauthorized, <<"Name or password is incorrect.">>});
             UserProps ->
-                UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<>>),
-                PasswordHash = hash_password(?l2b(Pass), UserSalt),
-                ExpectedHash = couch_util:get_value(<<"password_sha">>, UserProps,
nil),
-                case couch_util:verify(ExpectedHash, PasswordHash) of
+                case authenticate(?l2b(Pass), UserProps) of
                     true ->
                         Req#httpd{user_ctx=#user_ctx{
                             name=?l2b(User),
@@ -189,7 +186,7 @@ cookie_authentication_handler(#httpd{mochi_req=MochiReq}=Req) ->
                 ?LOG_DEBUG("timeout ~p", [Timeout]),
                 case (catch erlang:list_to_integer(TimeStr, 16)) of
                     TimeStamp when CurrentTime < TimeStamp + Timeout ->
-                        case couch_util:verify(ExpectedHash, Hash) of
+                        case couch_passwords:verify(ExpectedHash, Hash) of
                             true ->
                                 TimeLeft = TimeStamp + Timeout - CurrentTime,
                                 ?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
@@ -234,9 +231,6 @@ cookie_auth_cookie(Req, User, Secret, TimeStamp) ->
         couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
         [{path, "/"}] ++ cookie_scheme(Req) ++ max_age()).
 
-hash_password(Password, Salt) ->
-    ?l2b(couch_util:to_hex(crypto:sha(<<Password/binary, Salt/binary>>))).
-
 ensure_cookie_auth_secret() ->
     case couch_config:get("couch_httpd_auth", "secret", nil) of
         nil ->
@@ -270,9 +264,7 @@ handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
         Result -> Result
     end,
     UserSalt = couch_util:get_value(<<"salt">>, User, <<>>),
-    PasswordHash = hash_password(Password, UserSalt),
-    ExpectedHash = couch_util:get_value(<<"password_sha">>, User, nil),
-    case couch_util:verify(ExpectedHash, PasswordHash) of
+    case authenticate(Password, User) of
         true ->
             % setup the session cookie
             Secret = ?l2b(ensure_cookie_auth_secret()),
@@ -344,6 +336,20 @@ maybe_value(_Key, undefined, _Fun) -> [];
 maybe_value(Key, Else, Fun) ->
     [{Key, Fun(Else)}].
 
+authenticate(Pass, UserProps) ->
+    UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<>>),
+    {PasswordHash, ExpectedHash} =
+        case couch_util:get_value(<<"password_scheme">>, UserProps, <<"simple">>)
of
+        <<"simple">> ->
+            {couch_passwords:simple(Pass, UserSalt),
+            couch_util:get_value(<<"password_sha">>, UserProps, nil)};
+        <<"pbkdf2">> ->
+            Iterations = couch_util:get_value(<<"iterations">>, UserProps, 10000),
+            {couch_passwords:pbkdf2(Pass, UserSalt, Iterations),
+             couch_util:get_value(<<"derived_key">>, UserProps, nil)}
+    end,
+    couch_passwords:verify(PasswordHash, ExpectedHash).
+
 auth_name(String) when is_list(String) ->
     [_,_,_,_,_,Name|_] = re:split(String, "[\\W_]", [{return, list}]),
     ?l2b(Name).

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/src/couchdb/couch_passwords.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_passwords.erl b/src/couchdb/couch_passwords.erl
new file mode 100644
index 0000000..e5de878
--- /dev/null
+++ b/src/couchdb/couch_passwords.erl
@@ -0,0 +1,94 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_passwords).
+
+-export([simple/2, pbkdf2/3, pbkdf2/4, verify/2]).
+-include("couch_db.hrl").
+
+-define(MAX_DERIVED_KEY_LENGTH, (1 bsl 32 - 1)).
+-define(SHA1_OUTPUT_LENGTH, 20).
+
+%% legacy scheme, not used for new passwords.
+-spec simple(binary(), binary()) -> binary().
+simple(Password, Salt) ->
+    ?l2b(couch_util:to_hex(crypto:sha(<<Password/binary, Salt/binary>>))).
+
+%% Current scheme, much stronger.
+-spec pbkdf2(binary(), binary(), integer()) -> string().
+pbkdf2(Password, Salt, Iterations) ->
+    {ok, Result} = pbkdf2(Password, Salt, Iterations, ?SHA1_OUTPUT_LENGTH),
+    Result.
+
+-spec pbkdf2(binary(), binary(), integer(), integer())
+    -> {ok, binary()} | {error, derived_key_too_long}.
+pbkdf2(_Password, _Salt, _Iterations, DerivedLength)
+    when DerivedLength > ?MAX_DERIVED_KEY_LENGTH ->
+    {error, derived_key_too_long};
+pbkdf2(Password, Salt, Iterations, DerivedLength) ->
+    L = ceiling(DerivedLength / ?SHA1_OUTPUT_LENGTH),
+    <<Bin:DerivedLength/binary,_/binary>> =
+        iolist_to_binary(pbkdf2(Password, Salt, Iterations, L, 1, [])),
+    {ok, ?l2b(couch_util:to_hex(Bin))}.
+
+-spec pbkdf2(binary(), binary(), integer(), integer(), integer(), iolist())
+    -> iolist().
+pbkdf2(_Password, _Salt, _Iterations, BlockCount, BlockIndex, Acc)
+    when BlockIndex > BlockCount ->
+    lists:reverse(Acc);
+pbkdf2(Password, Salt, Iterations, BlockCount, BlockIndex, Acc) ->
+    Block = pbkdf2(Password, Salt, Iterations, BlockIndex, 1, <<>>, <<>>),
+    pbkdf2(Password, Salt, Iterations, BlockCount, BlockIndex + 1, [Block|Acc]).
+
+-spec pbkdf2(binary(), binary(), integer(), integer(), integer(),
+    binary(), binary()) -> binary().
+pbkdf2(_Password, _Salt, Iterations, _BlockIndex, Iteration, _Prev, Acc)
+    when Iteration > Iterations ->
+    Acc;
+pbkdf2(Password, Salt, Iterations, BlockIndex, 1, _Prev, _Acc) ->
+    InitialBlock = crypto:sha_mac(Password,
+        <<Salt/binary,BlockIndex:32/integer>>),
+    pbkdf2(Password, Salt, Iterations, BlockIndex, 2,
+        InitialBlock, InitialBlock);
+pbkdf2(Password, Salt, Iterations, BlockIndex, Iteration, Prev, Acc) ->
+    Next = crypto:sha_mac(Password, Prev),
+    pbkdf2(Password, Salt, Iterations, BlockIndex, Iteration + 1,
+                   Next, crypto:exor(Next, Acc)).
+
+%% verify two lists for equality without short-circuits to avoid timing attacks.
+-spec verify(string(), string(), integer()) -> boolean().
+verify([X|RestX], [Y|RestY], Result) ->
+    verify(RestX, RestY, (X bxor Y) bor Result);
+verify([], [], Result) ->
+    Result == 0.
+
+-spec verify(binary(), binary()) -> boolean();
+            (list(), list()) -> boolean().
+verify(<<X/binary>>, <<Y/binary>>) ->
+    verify(?b2l(X), ?b2l(Y));
+verify(X, Y) when is_list(X) and is_list(Y) ->
+    case length(X) == length(Y) of
+        true ->
+            verify(X, Y, 0);
+        false ->
+            false
+    end;
+verify(_X, _Y) -> false.
+
+-spec ceiling(number()) -> integer().
+ceiling(X) ->
+    T = erlang:trunc(X),
+    case (X - T) of
+        Neg when Neg < 0 -> T;
+        Pos when Pos > 0 -> T + 1;
+        _ -> T
+    end.

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/src/couchdb/couch_server.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl
index 332b44f..cf66b86 100644
--- a/src/couchdb/couch_server.erl
+++ b/src/couchdb/couch_server.erl
@@ -129,14 +129,19 @@ hash_admin_passwords() ->
     hash_admin_passwords(true).
 
 hash_admin_passwords(Persist) ->
+    Iterations = couch_config:get("couch_httpd_auth", "iterations", "10000"),
     lists:foreach(
         fun({_User, "-hashed-" ++ _}) ->
             ok; % already hashed
+        ({_User, "-pbkdf2-" ++ _}) ->
+            ok; % already hashed
         ({User, ClearPassword}) ->
-            Salt = ?b2l(couch_uuids:random()),
-            Hashed = couch_util:to_hex(crypto:sha(ClearPassword ++ Salt)),
+            Salt = couch_uuids:random(),
+            DerivedKey = couch_passwords:pbkdf2(ClearPassword, Salt,
+                list_to_integer(Iterations)),
             couch_config:set("admins",
-                User, "-hashed-" ++ Hashed ++ "," ++ Salt, Persist)
+                User, "-pbkdf2-" ++ ?b2l(DerivedKey) ++ "," ++ ?b2l(Salt) ++
+                                 "," ++ Iterations, Persist)
         end, couch_config:get("admins")).
 
 init([]) ->

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/src/couchdb/couch_users_db.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_users_db.erl b/src/couchdb/couch_users_db.erl
index adac719..6735fb6 100644
--- a/src/couchdb/couch_users_db.erl
+++ b/src/couchdb/couch_users_db.erl
@@ -18,7 +18,10 @@
 
 -define(NAME, <<"name">>).
 -define(PASSWORD, <<"password">>).
--define(PASSWORD_SHA, <<"password_sha">>).
+-define(DERIVED_KEY, <<"derived_key">>).
+-define(PASSWORD_SCHEME, <<"password_scheme">>).
+-define(PBKDF2, <<"pbkdf2">>).
+-define(ITERATIONS, <<"iterations">>).
 -define(SALT, <<"salt">>).
 -define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
 
@@ -60,10 +63,12 @@ save_doc(#doc{body={Body}} = Doc) ->
     undefined ->
         Doc;
     ClearPassword ->
-        Salt = ?b2l(couch_uuids:random()),
-        PasswordSha = couch_util:to_hex(crypto:sha(?b2l(ClearPassword) ++ Salt)),
-        Body1 = ?replace(Body, ?PASSWORD_SHA, ?l2b(PasswordSha)),
-        Body2 = ?replace(Body1, ?SALT, ?l2b(Salt)),
+        Iterations = list_to_integer(couch_config:get("couch_httpd_auth", "iterations", "1000")),
+        Salt = couch_uuids:random(),
+        DerivedKey = couch_passwords:pbkdf2(ClearPassword, Salt, Iterations),
+        Body0 = [{?PASSWORD_SCHEME, ?PBKDF2}, {?ITERATIONS, Iterations}|Body],
+        Body1 = ?replace(Body0, ?DERIVED_KEY, DerivedKey),
+        Body2 = ?replace(Body1, ?SALT, Salt),
         Body3 = proplists:delete(?PASSWORD, Body2),
         Doc#doc{body={Body3}}
     end.

http://git-wip-us.apache.org/repos/asf/couchdb/blob/7d418134/test/etap/230-pbkfd2.t
----------------------------------------------------------------------
diff --git a/test/etap/230-pbkfd2.t b/test/etap/230-pbkfd2.t
new file mode 100644
index 0000000..d980ef6
--- /dev/null
+++ b/test/etap/230-pbkfd2.t
@@ -0,0 +1,38 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+main(_) ->
+    test_util:init_code_path(),
+    etap:plan(6),
+    etap:is(couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 1,
20),
+            {ok, <<"0c60c80f961f0e71f3a9b524af6012062fe037a6">>},
+            "test vector #1"),
+    etap:is(couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 2,
20),
+            {ok, <<"ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957">>},
+            "test vector #2"),
+    etap:is(couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 4096,
20),
+            {ok, <<"4b007901b765489abead49d926f721d065a429c1">>},
+            "test vector #3"),
+    etap:is(couch_passwords:pbkdf2(<<"passwordPASSWORDpassword">>,
+                                                     <<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>,
4096, 25),
+            {ok, <<"3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038">>},
+            "test vector #4"),
+    etap:is(couch_passwords:pbkdf2(<<"pass\0word">>, <<"sa\0lt">>,
4096, 16),
+            {ok, <<"56fa6aa75548099dcc37d7f03425e0c3">>},
+            "test vector #5"),
+    etap:is(couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 16777216,
20),
+            {ok, <<"eefe3d61cd4da4e4e9945b3d6ba2158c2634e984">>},
+            "test vector #6"),
+    etap:end_tests().


Mime
View raw message