summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Lehnardt <jan@apache.org>2018-03-08 17:47:24 +0100
committerJan Lehnardt <jan@apache.org>2018-03-08 18:39:09 +0100
commit817b2b6f5f0883092df60c1ec8ec7ec6d6094a23 (patch)
tree9dce127981866e54ed3a94f8255699691505371b
parentba624ea7c26984b8ea52ec5e20534dc5f8508d3a (diff)
downloadcouchdb-817b2b6f5f0883092df60c1ec8ec7ec6d6094a23.tar.gz
Add bcrypt hashing option
-rw-r--r--.gitignore1
-rw-r--r--LICENSE117
-rw-r--r--NOTICE7
-rw-r--r--rebar.config.script6
-rw-r--r--rel/overlay/etc/default.ini3
-rw-r--r--rel/reltool.config2
-rw-r--r--src/couch/src/couch.app.src1
-rw-r--r--src/couch/src/couch.erl1
-rw-r--r--src/couch/src/couch_auth_cache.erl7
-rw-r--r--src/couch/src/couch_httpd_auth.erl12
-rw-r--r--src/couch/src/couch_passwords.erl19
-rw-r--r--src/couch/src/couch_users_db.erl10
-rw-r--r--src/couch/test/couch_passwords_tests.erl42
-rw-r--r--test/javascript/tests/users_db_security.js144
14 files changed, 350 insertions, 22 deletions
diff --git a/.gitignore b/.gitignore
index be121600c..3f5c4b284 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 e9a9c81e8..a209352a0 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 <hunter.morris@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 <provos@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 <provos@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 a1f06ae7b..c04033897 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 <hunter.morris@smarkets.com>
+ - Blowfish block cipher & bcrypt (hashing) code for OpenBSD, Copyright
+ 1997 Niels Provos <provos@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 c33a71cb5..a52350fc8 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 9762536ac..df4387735 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 464f5f4fe..aa3100647 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 8fc0d89ff..524b72868 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 fd5c9e101..f956b4b3d 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 157b0902e..425cce010 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 6ac7b75af..74cbe5a08 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 baf78f5d5..77e136144 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 c7b41f1fc..dd6d3208c 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 dea6d6b7b..c624dba5b 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 ad516a0c4..536585acf 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();
};