You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ja...@apache.org on 2018/03/08 17:39:13 UTC

[couchdb] branch master updated: Add bcrypt hashing option

This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 817b2b6  Add bcrypt hashing option
817b2b6 is described below

commit 817b2b6f5f0883092df60c1ec8ec7ec6d6094a23
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Thu Mar 8 17:47:24 2018 +0100

    Add bcrypt hashing option
---
 .gitignore                                 |   1 +
 LICENSE                                    | 117 +++++++++++++++++++++++
 NOTICE                                     |   7 ++
 rebar.config.script                        |   6 +-
 rel/overlay/etc/default.ini                |   3 +-
 rel/reltool.config                         |   2 +
 src/couch/src/couch.app.src                |   1 +
 src/couch/src/couch.erl                    |   1 +
 src/couch/src/couch_auth_cache.erl         |   7 ++
 src/couch/src/couch_httpd_auth.erl         |  12 ++-
 src/couch/src/couch_passwords.erl          |  19 +++-
 src/couch/src/couch_users_db.erl           |  10 +-
 src/couch/test/couch_passwords_tests.erl   |  42 ++++++++-
 test/javascript/tests/users_db_security.js | 144 ++++++++++++++++++++++++++---
 14 files changed, 350 insertions(+), 22 deletions(-)

diff --git a/.gitignore b/.gitignore
index be12160..3f5c4b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ share/server/main.js
 share/www
 src/b64url/
 src/bear/
+src/bcrypt/
 src/config/
 src/couch/priv/couch_js/config.h
 src/couch/priv/couchjs
diff --git a/LICENSE b/LICENSE
index e9a9c81..a209352 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2157,3 +2157,120 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
+
+The Erlang code is subject to this license:
+
+%% Copyright (c) 2011 Hunter Morris <hu...@smarkets.com>
+
+%% Permission to use, copy, modify, and distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+The underlying blowfish code is derived from OpenBSD libc and is
+subject to the following license:
+
+/*
+ * Blowfish block cipher for OpenBSD
+ * Copyright 1997 Niels Provos <pr...@physnet.uni-hamburg.de>
+ * All rights reserved.
+ *
+ * Implementation advice by David Mazieres <dm...@lcs.mit.edu>.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ *    must display the following acknowledgement:
+ *      This product includes software developed by Niels Provos.
+ * 4. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+The underlying bcrypt (hashing) code is derived from OpenBSD libc and is
+subject to the following license:
+
+/*
+ * Copyright 1997 Niels Provos <pr...@physnet.uni-hamburg.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ *    must display the following acknowledgement:
+ *      This product includes software developed by Niels Provos.
+ * 4. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+The asynchronous queue code (c_src/async_queue.c and
+c_src/async_queue.h) is from the esnappy project, copyright 2011
+Konstantin V. Sorokin. It is subject to the following license:
+
+Copyright (c) 2011 Konstantin V. Sorokin
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTOR(S) ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTOR(S) BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/NOTICE b/NOTICE
index a1f06ae..c040338 100644
--- a/NOTICE
+++ b/NOTICE
@@ -177,3 +177,10 @@ This product also includes the following third-party components:
 * velocity-react
 
   Copyright (c) 2015 Twitter, Inc.
+
+* erlang-bcrypt
+  - Erlang code: Copyright (c) 2011 Hunter Morris <hu...@smarkets.com>
+  - Blowfish block cipher & bcrypt (hashing) code for OpenBSD, Copyright
+    1997 Niels Provos <pr...@physnet.uni-hamburg.de>
+  - The asynchronous queue code (c_src/async_queue.c and c_src/async_queue.h)
+    is from the esnappy project, copyright 2011 Konstantin V. Sorokin.
diff --git a/rebar.config.script b/rebar.config.script
index c33a71c..a52350f 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -64,7 +64,9 @@ DepDescs = [
 {ibrowse,          "ibrowse",          {tag, "CouchDB-4.0.1"}},
 {jiffy,            "jiffy",            {tag, "CouchDB-0.14.11-2"}},
 {mochiweb,         "mochiweb",         {tag, "v2.17.0"}},
-{meck,             "meck",             {tag, "0.8.8"}}
+{meck,             "meck",             {tag, "0.8.8"}},
+{bcrypt,           {url, "https://github.com/apache/couchdb-erlang-bcrypt"},
+                   {tag, "1.0.2"}}
 
 ],
 
@@ -97,7 +99,7 @@ AddConfig = [
         {plt_location, local},
         {plt_location, COUCHDB_ROOT},
         {plt_extra_apps, [
-            asn1, compiler, crypto, inets, kernel, os_mon, runtime_tools,
+            asn1, bcrypt, compiler, crypto, inets, kernel, os_mon, runtime_tools,
             sasl, ssl, stdlib, syntax_tools, xmerl]},
         {warnings, [unmatched_returns, error_handling, race_conditions]}]},
     {post_hooks, [{compile, "escript support/build_js.escript"}]}
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 9762536..df43877 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -189,7 +189,8 @@ 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 = 10 ; iterations for password hashing
+iterations = 10 ; iterations for PBKDF2 password hashing
+log_rounds = 10 ; 2^log_rounds iterations for Bcrypt password hashing
 ; min_iterations = 1
 ; max_iterations = 1000000000
 ; password_scheme = pbkdf2
diff --git a/rel/reltool.config b/rel/reltool.config
index 464f5f4..aa31006 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -15,6 +15,7 @@
     {rel, "couchdb", "2.2.0", [
         %% stdlib
         asn1,
+        bcrypt,
         compiler,
         crypto,
         inets,
@@ -66,6 +67,7 @@
 
     %% stdlib
     {app, asn1, [{incl_cond, include}]},
+    {app, bcrypt, [{incl_cond, include}]},
     {app, compiler, [{incl_cond, include}]},
     {app, crypto, [{incl_cond, include}]},
     {app, inets, [{incl_cond, include}]},
diff --git a/src/couch/src/couch.app.src b/src/couch/src/couch.app.src
index 8fc0d89..524b728 100644
--- a/src/couch/src/couch.app.src
+++ b/src/couch/src/couch.app.src
@@ -31,6 +31,7 @@
         kernel,
         stdlib,
         crypto,
+        bcrypt,
         sasl,
         inets,
         ssl,
diff --git a/src/couch/src/couch.erl b/src/couch/src/couch.erl
index fd5c9e1..f956b4b 100644
--- a/src/couch/src/couch.erl
+++ b/src/couch/src/couch.erl
@@ -21,6 +21,7 @@ deps() ->
         inets,
         os_mon,
         crypto,
+        bcrypt,
         public_key,
         ssl,
         ibrowse,
diff --git a/src/couch/src/couch_auth_cache.erl b/src/couch/src/couch_auth_cache.erl
index 157b090..425cce0 100644
--- a/src/couch/src/couch_auth_cache.erl
+++ b/src/couch/src/couch_auth_cache.erl
@@ -92,6 +92,8 @@ get_admin(UserName) when is_list(UserName) ->
     "-pbkdf2-" ++ HashedPwdSaltAndIterations ->
         [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
         make_admin_doc(HashedPwd, Salt, Iterations);
+    "-bcrypt-" ++ HashedPwd ->
+        make_admin_doc(HashedPwd);
     _Else ->
 	nil
     end.
@@ -109,6 +111,11 @@ make_admin_doc(DerivedKey, Salt, Iterations) ->
      {<<"password_scheme">>, <<"pbkdf2">>},
      {<<"derived_key">>, ?l2b(DerivedKey)}].
 
+make_admin_doc(DerivedKey) ->
+    [{<<"roles">>, [<<"_admin">>]},
+     {<<"password_scheme">>, <<"bcrypt">>},
+     {<<"derived_key">>, ?l2b(DerivedKey)}].
+
 get_from_cache(UserName) ->
     exec_if_auth_db(
         fun(_AuthDb) ->
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 6ac7b75..74cbe5a 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -309,7 +309,12 @@ handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req, AuthModule) ->
             Secret = ?l2b(ensure_cookie_auth_secret()),
             UserSalt = couch_util:get_value(<<"salt">>, UserProps),
             CurrentTime = make_cookie_time(),
-            Cookie = cookie_auth_cookie(Req, ?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
+            Cookie = case UserSalt of
+                undefined ->
+                    cookie_auth_cookie(Req, ?b2l(UserName), <<Secret/binary>>, CurrentTime);
+                _ ->
+                    cookie_auth_cookie(Req, ?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime)
+            end,
             % TODO document the "next" feature in Futon
             {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
                 nil ->
@@ -401,7 +406,10 @@ authenticate(Pass, UserProps) ->
             Iterations = couch_util:get_value(<<"iterations">>, UserProps, 10000),
             verify_iterations(Iterations),
             {couch_passwords:pbkdf2(Pass, UserSalt, Iterations),
-             couch_util:get_value(<<"derived_key">>, UserProps, nil)}
+             couch_util:get_value(<<"derived_key">>, UserProps, nil)};
+        <<"bcrypt">> ->
+            UserHash = couch_util:get_value(<<"derived_key">>, UserProps, nil),
+            {couch_passwords:bcrypt(Pass, UserHash), UserHash}
     end,
     couch_passwords:verify(PasswordHash, ExpectedHash).
 
diff --git a/src/couch/src/couch_passwords.erl b/src/couch/src/couch_passwords.erl
index baf78f5..77e1361 100644
--- a/src/couch/src/couch_passwords.erl
+++ b/src/couch/src/couch_passwords.erl
@@ -12,7 +12,7 @@
 
 -module(couch_passwords).
 
--export([simple/2, pbkdf2/3, pbkdf2/4, verify/2]).
+-export([simple/2, pbkdf2/3, pbkdf2/4, bcrypt/2, verify/2]).
 -export([hash_admin_password/1, get_unhashed_admins/0]).
 
 -include_lib("couch/include/couch_db.hrl").
@@ -51,7 +51,10 @@ hash_admin_password("pbkdf2", ClearPassword) ->
                                         Salt ,list_to_integer(Iterations)),
     ?l2b("-pbkdf2-" ++ ?b2l(DerivedKey) ++ ","
         ++ ?b2l(Salt) ++ ","
-        ++ Iterations).
+        ++ Iterations);
+hash_admin_password("bcrypt", ClearPassword) ->
+    LogRounds = list_to_integer(config:get("couch_httpd_auth", "log_rounds", "10")),
+    ?l2b("-bcrypt-" ++ couch_passwords:bcrypt(couch_util:to_binary(ClearPassword), LogRounds)).
 
 -spec get_unhashed_admins() -> list().
 get_unhashed_admins() ->
@@ -60,6 +63,8 @@ get_unhashed_admins() ->
             false; % already hashed
         ({_User, "-pbkdf2-" ++ _}) ->
             false; % already hashed
+        ({_User, "-bcrypt-" ++ _}) ->
+            false; % already hashed
         ({_User, _ClearPassword}) ->
             true
         end,
@@ -123,6 +128,16 @@ pbkdf2(Password, Salt, Iterations, BlockIndex, Iteration, Prev, Acc) ->
     pbkdf2(Password, Salt, Iterations, BlockIndex, Iteration + 1,
                    Next, crypto:exor(Next, Acc)).
 
+%% Define the bcrypt functions to hash a password
+-spec bcrypt(binary(), binary()) -> binary();
+    (binary(), integer()) -> binary().
+bcrypt(Password, Salt) when is_binary(Salt) ->
+    {ok, Hash} = bcrypt:hashpw(Password, Salt),
+    list_to_binary(Hash);
+bcrypt(Password, LogRounds) when is_integer(LogRounds) ->
+    {ok, Salt} = bcrypt:gen_salt(LogRounds),
+    bcrypt(Password, list_to_binary(Salt)).
+
 %% verify two lists for equality without short-circuits to avoid timing attacks.
 -spec verify(string(), string(), integer()) -> boolean().
 verify([X|RestX], [Y|RestY], Result) ->
diff --git a/src/couch/src/couch_users_db.erl b/src/couch/src/couch_users_db.erl
index c7b41f1..dd6d320 100644
--- a/src/couch/src/couch_users_db.erl
+++ b/src/couch/src/couch_users_db.erl
@@ -23,6 +23,7 @@
 -define(SIMPLE, <<"simple">>).
 -define(PASSWORD_SHA, <<"password_sha">>).
 -define(PBKDF2, <<"pbkdf2">>).
+-define(BCRYPT, <<"bcrypt">>).
 -define(ITERATIONS, <<"iterations">>).
 -define(SALT, <<"salt">>).
 -define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
@@ -59,7 +60,7 @@ before_doc_update(Doc, Db) ->
 %    newDoc.salt = salt
 %    newDoc.password = null
 save_doc(#doc{body={Body}} = Doc) ->
-    %% Support both schemes to smooth migration from legacy scheme
+    %% Support all schemes to smooth migration from legacy scheme
     Scheme = config:get("couch_httpd_auth", "password_scheme", "pbkdf2"),
     case {couch_util:get_value(?PASSWORD, Body), Scheme} of
     {null, _} -> % server admins don't have a user-db password entry
@@ -84,6 +85,13 @@ save_doc(#doc{body={Body}} = Doc) ->
         Body3 = ?replace(Body2, ?SALT, Salt),
         Body4 = proplists:delete(?PASSWORD, Body3),
         Doc#doc{body={Body4}};
+    {ClearPassword, "bcrypt"} ->
+        LogRounds = list_to_integer(config:get("couch_httpd_auth", "log_rounds", "10")),
+        DerivedKey = couch_passwords:bcrypt(ClearPassword, LogRounds),
+        Body0 = ?replace(Body, ?PASSWORD_SCHEME, ?BCRYPT),
+        Body1 = ?replace(Body0, ?DERIVED_KEY, DerivedKey),
+        Body2 = proplists:delete(?PASSWORD, Body1),
+        Doc#doc{body={Body2}};
     {_ClearPassword, Scheme} ->
         couch_log:error("[couch_httpd_auth] password_scheme value of '~p' is invalid.", [Scheme]),
         throw({forbidden, "Server cannot hash passwords at this time."})
diff --git a/src/couch/test/couch_passwords_tests.erl b/src/couch/test/couch_passwords_tests.erl
index dea6d6b..c624dba 100644
--- a/src/couch/test/couch_passwords_tests.erl
+++ b/src/couch/test/couch_passwords_tests.erl
@@ -14,7 +14,6 @@
 
 -include_lib("couch/include/couch_eunit.hrl").
 
-
 pbkdf2_test_()->
     {"PBKDF2",
      [
@@ -52,3 +51,44 @@ pbkdf2_test_()->
                {ok, <<"eefe3d61cd4da4e4e9945b3d6ba2158c2634e984">>},
                couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 16777216, 20)
            )}}]}.
+
+
+
+setup() ->
+    test_util:start(?MODULE, [bcrypt]).
+
+teardown(Ctx)->
+    test_util:stop(Ctx).
+
+bcrypt_test_() ->
+    {
+        "Bcrypt",
+        {
+            foreach,
+            fun setup/0, fun teardown/1,
+            [
+                {timeout, 1, fun bcrypt_logRounds_4/0},
+                {timeout, 5, fun bcrypt_logRounds_12/0},
+                {timeout, 180, fun bcrypt_logRounds_18/0},
+                {timeout, 5, fun bcrypt_null_byte/0}
+
+            ]
+        }
+    }.
+
+bcrypt_logRounds_4() ->
+    bcrypt_assert_equal(<<"password">>, 4).
+
+bcrypt_logRounds_12() ->
+    bcrypt_assert_equal(<<"password">>, 12).
+
+bcrypt_logRounds_18() ->
+    bcrypt_assert_equal(<<"password">>, 18).
+
+bcrypt_null_byte() ->
+    bcrypt_assert_equal(<<"passw\0rd">>, 12).
+
+bcrypt_assert_equal(Password, Rounds) when is_integer(Rounds) ->
+    HashPass = couch_passwords:bcrypt(Password, Rounds),
+    ReHashPass = couch_passwords:bcrypt(Password, HashPass),
+    ?_assertEqual(HashPass, ReHashPass).
diff --git a/test/javascript/tests/users_db_security.js b/test/javascript/tests/users_db_security.js
index ad516a0..536585a 100644
--- a/test/javascript/tests/users_db_security.js
+++ b/test/javascript/tests/users_db_security.js
@@ -15,6 +15,8 @@ couchTests.users_db_security = function(debug) {
   var usersDb = new CouchDB(db_name, {"X-Couch-Full-Commit":"false"});
   try { usersDb.createDb(); } catch (e) { /* ignore if exists*/ }
 
+  var passwordSchemes = ['pbkdf2', 'bcrypt'];
+
   if (debug) debugger;
 
   var loginUser = function(username) {
@@ -86,7 +88,7 @@ couchTests.users_db_security = function(debug) {
     }
   };
 
-  var testFun = function()
+  var testFun = function(scheme, derivedKeyTest, saltTest)
   {
 
     // _users db
@@ -105,11 +107,13 @@ couchTests.users_db_security = function(debug) {
 
     // jan's gonna be admin as he's the first user
     TEquals(true, usersDb.save(userDoc).ok, "should save document");
-    wait(5000)
+    wait(5000);
     userDoc = open_as(usersDb, "org.couchdb.user:jchris", "jchris");
     TEquals(undefined, userDoc.password, "password field should be null 1");
-    TEquals(40, userDoc.derived_key.length, "derived_key should exist");
-    TEquals(32, userDoc.salt.length, "salt should exist");
+    TEquals(scheme, userDoc.password_scheme, "password_scheme should be " + scheme);
+    derivedKeyTest(userDoc.derived_key);
+    saltTest(userDoc.salt);
+
 
     // create server admin
 
@@ -141,10 +145,13 @@ couchTests.users_db_security = function(debug) {
     var jchrisDoc = open_as(usersDb, "org.couchdb.user:jchris", "jan");
 
     TEquals(undefined, jchrisDoc.password, "password field should be null 2");
-    TEquals(40, jchrisDoc.derived_key.length, "derived_key should exist");
-    TEquals(32, jchrisDoc.salt.length, "salt should exist");
+    TEquals(scheme, jchrisDoc.password_scheme, "password_scheme should be " + scheme);
+    derivedKeyTest(jchrisDoc.derived_key);
+    saltTest(jchrisDoc.salt);
 
-    TEquals(true, userDoc.salt != jchrisDoc.salt, "should have new salt");
+    if(userDoc.salt || jchrisDoc.salt) {
+      TEquals(true, userDoc.salt != jchrisDoc.salt, "should have new salt");
+    }
     TEquals(true, userDoc.derived_key != jchrisDoc.derived_key,
       "should have new derived_key");
 
@@ -227,7 +234,7 @@ couchTests.users_db_security = function(debug) {
       TEquals("forbidden", e.error, "non-admins can't read design docs");
     }
 
-    // admin shold be able to read _list
+    // admin should be able to read _list
     var listPath = ddoc["_id"] + "/_list/names/test";
     var result = request_as(usersDb, listPath, "jan");
     var lines = result.responseText.split("\n");
@@ -373,14 +380,124 @@ couchTests.users_db_security = function(debug) {
     });
   };
 
+  var derivedKeyTests = {
+    pbkdf2: function(derived_key) {
+      TEquals(40, derived_key.length, "derived_key should exist");
+    },
+    bcrypt: function(derived_key) {
+      TEquals(60, derived_key.length, "derived_key should exist");
+    }
+  };
+  var saltTests = {
+    pbkdf2: function(salt) {
+      TEquals(32, salt.length, "salt should exist");
+    },
+    bcrypt: function(salt) {
+      TEquals(undefined, salt, "salt should not exist");
+    }
+  };
+  passwordSchemes.forEach(function(scheme){
+    run_on_modified_server(
+      [{
+        section: "couch_httpd_auth",
+        key: "iterations", value: "1"
+      }, {
+        section: "couch_httpd_auth",
+        key: "password_scheme", value: scheme
+      }, {
+        section: "admins",
+        key: "jan", value: "apple"
+      }],
+      function() {
+        try {
+          testFun(scheme, derivedKeyTests[scheme], saltTests[scheme]);
+        } finally {
+          CouchDB.login("jan", "apple");
+          usersDb.deleteDb(); // cleanup
+          sleep(5000);
+          usersDb.createDb();
+        }
+      }
+    );
+  });
+
+  var testFunUpdatePasswordScheme = function() {
+    var userDocs = {
+      jchris: {
+        _id: "org.couchdb.user:jchris",
+        type: "user",
+        name: "jchris",
+        password: "mp3",
+        roles: []
+      },
+      fdmanana: {
+        _id: "org.couchdb.user:fdmanana",
+        type: "user",
+        name: "fdmanana",
+        password: "foobar",
+        roles: []
+      }
+    };
+
+    // create new user (has pbkdf2 hash)
+    TEquals(true, usersDb.save(userDocs.jchris).ok, "should save document");
+    wait(5000);
+    var userDoc = open_as(usersDb, "org.couchdb.user:jchris", "jchris");
+    TEquals(undefined, userDoc.password, "password field should be null 1");
+    TEquals("pbkdf2", userDoc.password_scheme, "password_scheme should be pbkdf2");
+    derivedKeyTests.pbkdf2(userDoc.derived_key);
+    saltTests.pbkdf2(userDoc.salt);
+
+    // change scheme to bcrypt
+    CouchDB.login("jan", "apple");
+    var xhr = CouchDB.request("PUT", "/_node/node1@127.0.0.1/_config/couch_httpd_auth/password_scheme", {
+      body : JSON.stringify("bcrypt"),
+      headers: {"X-Couch-Persist": "false"}
+    });
+    TEquals(200, xhr.status);
+    xhr = CouchDB.request("GET", "/_node/node1@127.0.0.1/_config/couch_httpd_auth/password_scheme");
+    var scheme = JSON.parse(xhr.responseText);
+    TEquals("bcrypt", scheme);
+
+    // create new user (has bcrypt hash)
+    TEquals(true, usersDb.save(userDocs.fdmanana).ok, "should save document");
+    wait(5000);
+    userDoc = open_as(usersDb, "org.couchdb.user:fdmanana", "fdmanana");
+    TEquals(undefined, userDoc.password, "password field should be null 1");
+    TEquals("bcrypt", userDoc.password_scheme, "password_scheme should be bcrypt");
+    derivedKeyTests.bcrypt(userDoc.derived_key);
+    saltTests.bcrypt(userDoc.salt);
+
+    // test that both users can still log in
+    TEquals(true, CouchDB.login(userDocs.jchris.name, userDocs.jchris.password).ok);
+    TEquals(true, CouchDB.login(userDocs.fdmanana.name, userDocs.fdmanana.password).ok);
+
+    // change scheme back to pbkdf2
+    CouchDB.login("jan", "apple");
+    var xhr = CouchDB.request("PUT", "/_node/node1@127.0.0.1/_config/couch_httpd_auth/password_scheme", {
+        body : JSON.stringify("pbkdf2"),
+        headers: {"X-Couch-Persist": "false"}
+    });
+    TEquals(200, xhr.status);
+    xhr = CouchDB.request("GET", "/_node/node1@127.0.0.1/_config/couch_httpd_auth/password_scheme");
+    var scheme = JSON.parse(xhr.responseText);
+    TEquals("pbkdf2", scheme);
+
+    // test that both users can still log in
+    TEquals(true, CouchDB.login(userDocs.jchris.name, userDocs.jchris.password).ok);
+    TEquals(true, CouchDB.login(userDocs.fdmanana.name, userDocs.fdmanana.password).ok);
+  };
   run_on_modified_server(
-    [{section: "couch_httpd_auth",
-      key: "iterations", value: "1"},
-   {section: "admins",
-    key: "jan", value: "apple"}],
+    [{
+      section: "couch_httpd_auth",
+      key: "iterations", value: "1"
+    }, {
+      section: "admins",
+      key: "jan", value: "apple"
+    }],
     function() {
       try {
-        testFun();
+        testFunUpdatePasswordScheme();
       } finally {
         CouchDB.login("jan", "apple");
         usersDb.deleteDb(); // cleanup
@@ -389,5 +506,6 @@ couchTests.users_db_security = function(debug) {
       }
     }
   );
+
   CouchDB.logout();
 };

-- 
To stop receiving notification emails like this one, please contact
jan@apache.org.