Skip to content

Commit

Permalink
Merge pull request #4976 from apache/couch_passwords_metrics
Browse files Browse the repository at this point in the history
add metrics for fast vs slow password hashing
  • Loading branch information
rnewson committed Feb 8, 2024
2 parents 2501fe6 + 67d3c08 commit 66c34e2
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/couch/priv/stats_descriptions.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@
{type, counter},
{desc, <<"number of file descriptors CouchDB has open">>}
]}.
{[couchdb, password_hashing_fast], [
{type, counter},
{desc, <<"number of fast password hashing operations">>}
]}.
{[couchdb, password_hashing_slow], [
{type, counter},
{desc, <<"number of slow password hashing operations">>}
]}.
{[couchdb, request_time], [
{type, histogram},
{desc, <<"length of a request inside CouchDB without MochiWeb">>}
Expand Down
1 change: 1 addition & 0 deletions src/couch/src/couch_passwords.erl
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pbkdf2(PRF, Password, Salt, Iterations, KeyLen) when
Iterations > 0,
KeyLen > 0
->
couch_stats:increment_counter([couchdb, password_hashing_slow]),
DerivedKey = fast_pbkdf2:pbkdf2(PRF, Password, Salt, Iterations, KeyLen),
couch_util:to_hex_bin(DerivedKey);
pbkdf2(PRF, Password, Salt, Iterations, KeyLen) when
Expand Down
1 change: 1 addition & 0 deletions src/couch/src/couch_passwords_cache.erl
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ insert(AuthModule, UserName, Password, Salt) when
end.

hash(Password, Salt) ->
couch_stats:increment_counter([couchdb, password_hashing_fast]),
fast_pbkdf2:pbkdf2(sha256, Password, Salt, ?FAST_ITERATIONS, ?SHA256_OUTPUT_LEN).
3 changes: 3 additions & 0 deletions test/elixir/test/config/suite.elixir
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,9 @@
"query with update=false works",
"view updates properly remove old keys"
],
"PasswordCacheTest": [
"password cache"
],
"ProxyAuthTest": [
"proxy auth with secret",
"proxy auth without secret"
Expand Down
140 changes: 140 additions & 0 deletions test/elixir/test/password_cache_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
defmodule PasswordCacheTest do
use CouchTestCase

@moduletag :authentication

@tag :with_db
test "password cache", context do
db_name = context[:db_name]

server_config = [
%{
:section => "chttpd_auth",
:key => "authentication_db",
:value => db_name
},
%{
:section => "httpd",
:key => "authentication_handlers",
:value => "{couch_httpd_auth, default_authentication_handler}"
}
]

run_on_modified_server(server_config, fn -> test_fun(db_name) end)
end

defp fast() do
hits = request_stats(["couchdb", "password_hashing_fast"], true)
hits["value"] || 0
end

defp slow() do
misses = request_stats(["couchdb", "password_hashing_slow"], true)
misses["value"] || 0
end

defp logout(session) do
assert Couch.Session.logout(session).body["ok"]
end

defp login_fail(user, password) do
resp = Couch.login(user, password, :fail)
assert resp.error, "Login error is expected."
end

defp login(user, password) do
sess = Couch.login(user, password)
assert sess.cookie, "Login correct is expected"
sess
end

defp assert_cache(event, user, password, expect \\ :expect_login_success) do
slow_before = slow()
fast_before = fast()

session =
case expect do
:expect_login_success -> login(user, password)
:expect_login_fail -> login_fail(user, password)
_ -> assert false
end

slow_after = slow()
fast_after = fast() - 3 # ignore the 3 fast bumps for previous _stats calls

if expect == :expect_success do
logout(session)
end

case event do
:expect_fast ->
assert fast_after == fast_before + 1,
"Fast hash is expected for #{user} during login"

assert slow_after == slow_before,
"No slow hash is expected for #{user} during login"

:expect_slow ->
assert slow_after == slow_before + 1,
"Slow hash is expected for #{user} during login"

case expect do
:expect_login_success ->
assert fast_after == fast_before + 1,
"Fast hash is expected after successful auth after slow hash for #{user} during login"
:expect_login_fail ->
assert fast_after == fast_before
assert fast_after == fast_before,
"No fast hash is expected after unsuccessful auth after slow hash for #{user} during login"
_ ->
assert false
end
_ ->
assert false
end
end

def save_doc(db_name, body) do
resp = Couch.put("/#{db_name}/#{body["_id"]}", body: body)
assert resp.status_code in [201, 202]
assert resp.body["ok"]
Map.put(body, "_rev", resp.body["rev"])
end

def make_user(db_name, user_name) do
result = prepare_user_doc([
{:name, user_name},
{:password, user_name}
])
{:ok, resp} = create_doc(db_name, result)
Map.put(result, "_rev", resp.body["rev"])
end

defp test_fun(db_name) do
user1 = make_user(db_name, "user1")

# cache misses mean slow auth
assert_cache(:expect_slow, "user1", "wrong_password", :expect_login_fail)
assert_cache(:expect_slow, "user1", "wrong_password", :expect_login_fail)

# last slow auth that populates the cache
assert_cache(:expect_slow, "user1", "user1")

# Fast while cached
assert_cache(:expect_fast, "user1", "user1")
assert_cache(:expect_fast, "user1", "user1")

# change password
user1 = Map.replace!(user1, "password", "new_password")
save_doc(db_name, user1)

# Wait for auth cache to notice password change
:timer.sleep(5000)

# Slow rejection for wrong password
assert_cache(:expect_slow, "user1", "wrong_password", :expect_login_fail)

# Slow acceptance for new, uncached password
assert_cache(:expect_slow, "user1", "new_password")
end
end

0 comments on commit 66c34e2

Please sign in to comment.