Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

COUCHDB-1060 - Switch to PBKDF2 for new passwords

  • Loading branch information...
commit 7d4181346626c0cdb50b44f7e5e33435a8ccae0f 1 parent 394a08a
@rnewson rnewson authored
View
5 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
-------------
View
1  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
-------------
View
1  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
View
2  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 \
View
29 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(
View
28 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).
View
94 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.
View
11 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([]) ->
View
15 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.
View
38 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().
Please sign in to comment.
Something went wrong with that request. Please try again.