Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id A1D96998C for ; Fri, 16 Mar 2012 12:49:34 +0000 (UTC) Received: (qmail 79767 invoked by uid 500); 16 Mar 2012 12:49:34 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 79689 invoked by uid 500); 16 Mar 2012 12:49:33 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 79678 invoked by uid 99); 16 Mar 2012 12:49:33 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 16 Mar 2012 12:49:33 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 0F14E7536; Fri, 16 Mar 2012 12:49:32 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: rnewson@apache.org To: commits@couchdb.apache.org X-Mailer: ASF-Git Admin Mailer Subject: git commit: Switch to PBKDF2 for new passwords Message-Id: <20120316124933.0F14E7536@tyr.zones.apache.org> Date: Fri, 16 Mar 2012 12:49:32 +0000 (UTC) Updated Branches: refs/heads/COUCHDB-1060-strong-password-hash [created] 7362b5f22 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/7362b5f2 Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/7362b5f2 Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/7362b5f2 Branch: refs/heads/COUCHDB-1060-strong-password-hash Commit: 7362b5f22889954f4e363da377bc3709c2d39672 Parents: fd1753e Author: Robert Newson Authored: Fri Mar 16 12:22:37 2012 +0000 Committer: Robert Newson Committed: Fri Mar 16 12:45:52 2012 +0000 ---------------------------------------------------------------------- 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 +++++++++++++ 8 files changed, 192 insertions(+), 26 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb/blob/7362b5f2/etc/couchdb/default.ini.tpl.in ---------------------------------------------------------------------- diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index cebf242..6931c97 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 = 1000 ; 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/7362b5f2/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/7362b5f2/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/7362b5f2/src/couchdb/couch_httpd_auth.erl ---------------------------------------------------------------------- diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl index c09823c..855e895 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(<>))). - 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, <<>>), + {ExpectedHash, PasswordHash} = + 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, 1000), + {couch_passwords:pbkdf2(Pass, UserSalt, Iterations), + couch_util:get_value(<<"derived_key">>, UserProps, nil)} + end, + couch_passwords:verify(ExpectedHash, PasswordHash). + 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/7362b5f2/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(<>))). + +%% 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), + <> = + 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, + <>), + 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(<>, <>) -> + 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/7362b5f2/src/couchdb/couch_server.erl ---------------------------------------------------------------------- diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl index 332b44f..7e9aa05 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", "1000"), 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/7362b5f2/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/7362b5f2/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().