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

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

commit 2d2e65dc5fa04d2f11cf12ea55db3828aea73203
Author: Robert Newson <rnew...@apache.org>
AuthorDate: Fri Oct 27 14:00:22 2023 +0100

    Password hash upgrade tests
---
 test/elixir/lib/couch/dbtest.ex             |   8 +-
 test/elixir/test/config/skip.elixir         |   3 -
 test/elixir/test/helper_test.exs            |   2 +-
 test/elixir/test/users_db_security_test.exs | 120 +++++++++++++++++++++++-----
 4 files changed, 106 insertions(+), 27 deletions(-)

diff --git a/test/elixir/lib/couch/dbtest.ex b/test/elixir/lib/couch/dbtest.ex
index ae59da09f..a833582a7 100644
--- a/test/elixir/lib/couch/dbtest.ex
+++ b/test/elixir/lib/couch/dbtest.ex
@@ -416,11 +416,11 @@ defmodule Couch.DBTest do
     retry_until(condition, now(:ms), sleep, timeout)
   end
 
-  defp retry_until(condition, start, sleep, timeout) do
+  defp retry_until(condition, start, sleep, timeout, err \\ nil) do
     now = now(:ms)
 
     if now > start + timeout do
-      raise "timed out after #{now - start} ms"
+      raise err
     else
       try do
         if result = condition.() do
@@ -429,9 +429,9 @@ defmodule Couch.DBTest do
           raise ExUnit.AssertionError
         end
       rescue
-        ExUnit.AssertionError ->
+        e in ExUnit.AssertionError ->
           :timer.sleep(sleep)
-          retry_until(condition, start, sleep, timeout)
+          retry_until(condition, start, sleep, timeout, e)
       end
     end
   end
diff --git a/test/elixir/test/config/skip.elixir 
b/test/elixir/test/config/skip.elixir
index 7fbcacef7..be427a740 100644
--- a/test/elixir/test/config/skip.elixir
+++ b/test/elixir/test/config/skip.elixir
@@ -13,9 +13,6 @@
   "SecurityValidationTest": [
     "Author presence and user security when replicated"
   ],
-  "UsersDbSecurityTest": [
-    "user db security"
-  ],
   "WithQuorumTest": [
     "Creating/Deleting DB should return 201-Created/202-Acepted"
   ],
diff --git a/test/elixir/test/helper_test.exs b/test/elixir/test/helper_test.exs
index 19d70eac8..e3702a824 100644
--- a/test/elixir/test/helper_test.exs
+++ b/test/elixir/test/helper_test.exs
@@ -18,7 +18,7 @@ defmodule HelperTest do
   end
 
   test "retry_until times out", _context do
-    assert_raise RuntimeError, ~r/^timed out after \d+ ms$/, fn ->
+    assert_raise ExUnit.AssertionError, "\n\nExpected truthy, got false\ncode: 
assert false\n", fn ->
       retry_until(
         fn ->
           assert false
diff --git a/test/elixir/test/users_db_security_test.exs 
b/test/elixir/test/users_db_security_test.exs
index 656749040..428541353 100644
--- a/test/elixir/test/users_db_security_test.exs
+++ b/test/elixir/test/users_db_security_test.exs
@@ -91,10 +91,11 @@ defmodule UsersDbSecurityTest do
   defp save_as(db_name, doc, options) do
     use_session = Keyword.get(options, :use_session)
     user = Keyword.get(options, :user)
+    pwd = Keyword.get(options, :pwd)
     expect_response = Keyword.get(options, :expect_response, [201, 202])
     expect_message = Keyword.get(options, :error_message)
 
-    session = use_session || login_as(user)
+    session = use_session || login_as(user, pwd)
 
     resp =
       Couch.Session.put(
@@ -276,7 +277,7 @@ defmodule UsersDbSecurityTest do
     # Exising 'derived_key' and 'salt' fields are overwritten with new values
     # when a non-null 'password' field exists.
     # anonymous should be able to create a user document
-    user_doc = %{
+    tom_doc1 = %{
       _id: "org.couchdb.user:tom",
       type: "user",
       name: "tom",
@@ -285,18 +286,18 @@ defmodule UsersDbSecurityTest do
     }
 
     resp =
-      Couch.post("/#{@users_db}", body: user_doc, headers: [authorization: 
"annonymous"])
+      Couch.post("/#{@users_db}", body: tom_doc1, headers: [authorization: 
"annonymous"])
 
     assert resp.status_code in [201, 202]
     assert resp.body["ok"]
 
-    user_doc =
+    _tom_doc2 =
       retry_until(fn ->
-        user_doc = open_as(@users_db, "org.couchdb.user:tom", user: "tom")
-        assert !user_doc["password"]
-        assert String.length(user_doc["derived_key"]) == 40
-        assert String.length(user_doc["salt"]) == 32
-        user_doc
+        doc = open_as(@users_db, "org.couchdb.user:tom", user: "tom")
+        assert !doc["password"]
+        assert String.length(doc["derived_key"]) == 64
+        assert String.length(doc["salt"]) == 32
+        doc
       end)
 
     # anonymous should not be able to read an existing user's user document
@@ -313,8 +314,8 @@ defmodule UsersDbSecurityTest do
     assert resp.body["error"] == "unauthorized"
 
     # user should be able to read their own document
-    tom_doc = open_as(@users_db, "org.couchdb.user:tom", user: "tom")
-    assert tom_doc["_id"] == "org.couchdb.user:tom"
+    tom_doc3 = open_as(@users_db, "org.couchdb.user:tom", user: "tom")
+    assert tom_doc3["_id"] == "org.couchdb.user:tom"
 
     # user should not be able to read /_users/_changes
     changes_as(@users_db,
@@ -323,15 +324,96 @@ defmodule UsersDbSecurityTest do
       expect_message: "unauthorized"
     )
 
-    tom_doc = Map.put(tom_doc, "password", "couch")
-    save_as(@users_db, tom_doc, user: "tom")
+    # changing password regenerates derived_key and salt
+    tom_doc4 = Map.put(tom_doc3, "password", "couch")
+    save_as(@users_db, tom_doc4, user: "tom")
+
+    _tom_doc5 =
+      retry_until(fn ->
+        doc = open_as(@users_db, "org.couchdb.user:tom", user: "jerry")
+        assert !doc["password"]
+        assert String.length(doc["derived_key"]) == 64
+        assert String.length(doc["salt"]) == 32
+        assert doc["iterations"] == 1
+        assert doc["pbkdf2_prf"] == "sha256"
+        assert doc["derived_key"] != tom_doc4["derived_key"]
+        assert doc["salt"] != tom_doc4["salt"]
+        doc
+      end)
+
+    # changing iteration count regenerates derived_key, salt and iterations
+    # on next password change
+    set_config({
+      "chttpd_auth",
+      "iterations",
+      "3"
+    })
+
+    tom_doc5 = Map.put(tom_doc4, "password", "couch")
+    # 201 if the save does the update, 409 if the async password hasher got 
there first.
+    save_as(@users_db, tom_doc5, user: "tom", pwd: "couch", expect_response: 
[201, 409])
 
-    tom_doc = open_as(@users_db, "org.couchdb.user:tom", user: "jerry")
-    assert !tom_doc["password"]
-    assert String.length(tom_doc["derived_key"]) == 40
-    assert String.length(tom_doc["salt"]) == 32
-    assert tom_doc["derived_key"] != user_doc["derived_key"]
-    assert tom_doc["salt"] != user_doc["salt"]
+    tom_doc6 =
+      retry_until(fn ->
+        doc = open_as(@users_db, "org.couchdb.user:tom", user: "jerry")
+        assert !doc["password"]
+        assert String.length(doc["derived_key"]) == 64
+        assert String.length(doc["salt"]) == 32
+        assert doc["iterations"] == 3
+        assert doc["pbkdf2_prf"] == "sha256"
+        assert doc["derived_key"] != tom_doc5["derived_key"]
+        assert doc["salt"] != tom_doc5["salt"]
+        doc
+      end)
+
+    # changing PRF regenerates derived_key and salt on next
+    # password change
+    set_config({
+      "chttpd_auth",
+      "pbkdf2_prf",
+      "sha512"
+    })
+
+    tom_doc7 = Map.put(tom_doc6, "password", "couch")
+    # 201 if the save does the update, 409 if the async password hasher got 
there first.
+    save_as(@users_db, tom_doc7, user: "tom", pwd: "couch", expect_response: 
[201, 409])
+
+    tom_doc8 =
+      retry_until(fn ->
+        doc = open_as(@users_db, "org.couchdb.user:tom", user: "jerry")
+        assert !doc["password"]
+        assert String.length(doc["derived_key"]) == 128
+        assert String.length(doc["salt"]) == 32
+        assert doc["iterations"] == 3
+        assert doc["pbkdf2_prf"] == "sha512"
+        assert doc["derived_key"] != tom_doc7["derived_key"]
+        assert doc["salt"] != tom_doc7["salt"]
+        doc
+      end)
+
+    # changing iterations regenerates derived_key and preserves salt
+    # on next successful authentication
+    set_config({
+      "chttpd_auth",
+      "iterations",
+      "4"
+    })
+
+    _tom_doc9 =
+      retry_until(fn ->
+        # login inside the loop as we need to trigger the async
+        # hasher each time
+        assert login_as("tom", "couch")
+        doc = open_as(@users_db, "org.couchdb.user:tom", user: "jerry")
+        assert !doc["password"]
+        assert String.length(doc["derived_key"]) == 128
+        assert String.length(doc["salt"]) == 32
+        assert doc["iterations"] == 4
+        assert doc["pbkdf2_prf"] == "sha512"
+        assert doc["derived_key"] != tom_doc8["derived_key"]
+        assert doc["salt"] == tom_doc8["salt"]
+        doc
+      end)
 
     # user should not be able to read another user's user document
     spike_doc = %{

Reply via email to