You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2023/10/22 16:30:59 UTC

[couchdb] 01/02: increase default iterations for new credentials

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 3ce67797b9412d2ece8d93bfd9d9b42cae6faf3e
Author: Robert Newson <rn...@apache.org>
AuthorDate: Fri Oct 20 15:56:07 2023 +0100

    increase default iterations for new credentials
---
 dev/pbkdf2.py                     | 201 --------------------------------------
 dev/run                           |   9 +-
 rel/overlay/etc/default.ini       |   2 +-
 src/couch/src/couch_passwords.erl |   2 +-
 src/couch/src/couch_users_db.erl  |   2 +-
 test/elixir/lib/couch.ex          |   1 +
 test/elixir/test/config_test.exs  |   2 +
 7 files changed, 10 insertions(+), 209 deletions(-)

diff --git a/dev/pbkdf2.py b/dev/pbkdf2.py
deleted file mode 100644
index 4416f8632..000000000
--- a/dev/pbkdf2.py
+++ /dev/null
@@ -1,201 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    pbkdf2
-    ~~~~~~
-
-    This module implements pbkdf2 for Python.  It also has some basic
-    tests that ensure that it works.  The implementation is straightforward
-    and uses stdlib only stuff and can be easily be copy/pasted into
-    your favourite application.
-
-    Use this as replacement for bcrypt that does not need a c implementation
-    of a modified blowfish crypto algo.
-
-    Example usage:
-
-    >>> pbkdf2_hex('what i want to hash', 'the random salt')
-    'fa7cc8a2b0a932f8e6ea42f9787e9d36e592e0c222ada6a9'
-
-    How to use this:
-
-    1.  Use a constant time string compare function to compare the stored hash
-        with the one you're generating::
-
-            def safe_str_cmp(a, b):
-                if len(a) != len(b):
-                    return False
-                rv = 0
-                for x, y in izip(a, b):
-                    rv |= ord(x) ^ ord(y)
-                return rv == 0
-
-    2.  Use `os.urandom` to generate a proper salt of at least 8 byte.
-        Use a unique salt per hashed password.
-
-    3.  Store ``algorithm$salt:costfactor$hash`` in the database so that
-        you can upgrade later easily to a different algorithm if you need
-        one.  For instance ``PBKDF2-256$thesalt:10000$deadbeef...``.
-
-
-    :copyright: (c) Copyright 2011 by Armin Ronacher.
-    :license: BSD, see LICENSE for more details.
-"""
-from binascii import hexlify
-import hmac
-import hashlib
-import sys
-from struct import Struct
-from operator import xor
-from itertools import starmap
-
-PY3 = sys.version_info[0] == 3
-
-if not PY3:
-    from itertools import izip as zip
-
-if PY3:
-    text_type = str
-else:
-    text_type = unicode
-
-
-_pack_int = Struct(">I").pack
-
-
-def bytes_(s, encoding="utf8", errors="strict"):
-    if isinstance(s, text_type):
-        return s.encode(encoding, errors)
-    return s
-
-
-def hexlify_(s):
-    if PY3:
-        return str(hexlify(s), encoding="utf8")
-    else:
-        return s.encode("hex")
-
-
-def range_(*args):
-    if PY3:
-        return range(*args)
-    else:
-        return xrange(*args)
-
-
-def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None):
-    """Like :func:`pbkdf2_bin` but returns a hex encoded string."""
-    return hexlify_(pbkdf2_bin(data, salt, iterations, keylen, hashfunc))
-
-
-def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None):
-    """Returns a binary digest for the PBKDF2 hash algorithm of `data`
-    with the given `salt`.  It iterates `iterations` time and produces a
-    key of `keylen` bytes.  By default SHA-1 is used as hash function,
-    a different hashlib `hashfunc` can be provided.
-    """
-    hashfunc = hashfunc or hashlib.sha1
-    mac = hmac.new(bytes_(data), None, hashfunc)
-
-    def _pseudorandom(x, mac=mac):
-        h = mac.copy()
-        h.update(bytes_(x))
-        if PY3:
-            return [x for x in h.digest()]
-        else:
-            return map(ord, h.digest())
-
-    buf = []
-    for block in range_(1, -(-keylen // mac.digest_size) + 1):
-        rv = u = _pseudorandom(bytes_(salt) + _pack_int(block))
-        for i in range_(iterations - 1):
-            if PY3:
-                u = _pseudorandom(bytes(u))
-            else:
-                u = _pseudorandom("".join(map(chr, u)))
-            rv = starmap(xor, zip(rv, u))
-        buf.extend(rv)
-    if PY3:
-        return bytes(buf)[:keylen]
-    else:
-        return "".join(map(chr, buf))[:keylen]
-
-
-def test():
-    failed = []
-
-    def check(data, salt, iterations, keylen, expected):
-        rv = pbkdf2_hex(data, salt, iterations, keylen)
-        if rv != expected:
-            print("Test failed:")
-            print("  Expected:   %s" % expected)
-            print("  Got:        %s" % rv)
-            print("  Parameters:")
-            print("    data=%s" % data)
-            print("    salt=%s" % salt)
-            print("    iterations=%d" % iterations)
-            failed.append(1)
-
-    # From RFC 6070
-    check("password", "salt", 1, 20, "0c60c80f961f0e71f3a9b524af6012062fe037a6")
-    check("password", "salt", 2, 20, "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957")
-    check("password", "salt", 4096, 20, "4b007901b765489abead49d926f721d065a429c1")
-    check(
-        "passwordPASSWORDpassword",
-        "saltSALTsaltSALTsaltSALTsaltSALTsalt",
-        4096,
-        25,
-        "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",
-    )
-    check("pass\x00word", "sa\x00lt", 4096, 16, "56fa6aa75548099dcc37d7f03425e0c3")
-    # This one is from the RFC but it just takes for ages
-    ##check('password', 'salt', 16777216, 20,
-    ##      'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984')
-
-    # From Crypt-PBKDF2
-    check(
-        "password", "ATHENA.MIT.EDUraeburn", 1, 16, "cdedb5281bb2f801565a1122b2563515"
-    )
-    check(
-        "password",
-        "ATHENA.MIT.EDUraeburn",
-        1,
-        32,
-        "cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837",
-    )
-    check(
-        "password", "ATHENA.MIT.EDUraeburn", 2, 16, "01dbee7f4a9e243e988b62c73cda935d"
-    )
-    check(
-        "password",
-        "ATHENA.MIT.EDUraeburn",
-        2,
-        32,
-        "01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86",
-    )
-    check(
-        "password",
-        "ATHENA.MIT.EDUraeburn",
-        1200,
-        32,
-        "5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13",
-    )
-    check(
-        "X" * 64,
-        "pass phrase equals block size",
-        1200,
-        32,
-        "139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1",
-    )
-    check(
-        "X" * 65,
-        "pass phrase exceeds block size",
-        1200,
-        32,
-        "9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a",
-    )
-
-    raise SystemExit(bool(failed))
-
-
-if __name__ == "__main__":
-    test()
diff --git a/dev/run b/dev/run
index 823206ae0..dd2fd1d52 100755
--- a/dev/run
+++ b/dev/run
@@ -16,6 +16,7 @@ import atexit
 import base64
 import contextlib
 import functools
+import hashlib
 import glob
 import inspect
 import json
@@ -33,8 +34,6 @@ import uuid
 import traceback
 from configparser import ConfigParser
 
-from pbkdf2 import pbkdf2_hex
-
 COMMON_SALT = uuid.uuid4().hex
 
 try:
@@ -560,7 +559,7 @@ def gen_password():
     return base64.b64encode(os.urandom(6)).decode()
 
 
-def hashify(pwd, salt=COMMON_SALT, iterations=10, keylen=20):
+def hashify(pwd, salt=COMMON_SALT, iterations=50000, keylen=32):
     """
     Implements password hashing according to:
       - https://issues.apache.org/jira/browse/COUCHDB-1060
@@ -571,8 +570,8 @@ def hashify(pwd, salt=COMMON_SALT, iterations=10, keylen=20):
     >>> hashify(candeira)
     -pbkdf2-99eb34d97cdaa581e6ba7b5386e112c265c5c670,d1d2d4d8909c82c81b6c8184429a0739,10
     """
-    derived_key = pbkdf2_hex(pwd, salt, iterations, keylen)
-    return "-pbkdf2-%s,%s,%s" % (derived_key, salt, iterations)
+    derived_key = hashlib.pbkdf2_hmac('sha256', bytes(pwd, 'utf-8'), bytes(salt, 'utf-8'), iterations, keylen).hex()
+    return "-pbkdf2:sha256-%s,%s,%s" % (derived_key, salt, iterations)
 
 
 def startup(ctx):
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 1604f1585..e93916901 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -321,7 +321,7 @@ bind_address = 127.0.0.1
 ;timeout = 600 ; number of seconds before automatic logout
 ;auth_cache_size = 50 ; size is number of cache entries
 ;allow_persistent_cookies = true ; set to false to disallow persistent cookies
-;iterations = 10 ; iterations for password hashing
+;iterations = 50000 ; iterations for password hashing
 ;min_iterations = 1
 ;max_iterations = 1000000000
 ;password_scheme = pbkdf2
diff --git a/src/couch/src/couch_passwords.erl b/src/couch/src/couch_passwords.erl
index 7a67942d3..bf17709f6 100644
--- a/src/couch/src/couch_passwords.erl
+++ b/src/couch/src/couch_passwords.erl
@@ -44,7 +44,7 @@ hash_admin_password("simple", ClearPassword) ->
     ?l2b("-hashed-" ++ couch_util:to_hex(Hash) ++ "," ++ ?b2l(Salt));
 hash_admin_password("pbkdf2", ClearPassword) ->
     PRF = chttpd_util:get_chttpd_auth_config("pbkdf2_prf", "sha256"),
-    Iterations = chttpd_util:get_chttpd_auth_config("iterations", "10"),
+    Iterations = chttpd_util:get_chttpd_auth_config("iterations", "50000"),
     Salt = couch_uuids:random(),
     DerivedKey = couch_passwords:pbkdf2(
         list_to_existing_atom(PRF),
diff --git a/src/couch/src/couch_users_db.erl b/src/couch/src/couch_users_db.erl
index f8d56882a..3dc3dffde 100644
--- a/src/couch/src/couch_users_db.erl
+++ b/src/couch/src/couch_users_db.erl
@@ -84,7 +84,7 @@ save_doc(#doc{body = {Body}} = Doc) ->
             ok = validate_password(ClearPassword),
             PRF = chttpd_util:get_chttpd_auth_config("pbkdf2_prf", "sha256"),
             Iterations = chttpd_util:get_chttpd_auth_config_integer(
-                "iterations", 10
+                "iterations", 50000
             ),
             Salt = couch_uuids:random(),
             DerivedKey = couch_passwords:pbkdf2(
diff --git a/test/elixir/lib/couch.ex b/test/elixir/lib/couch.ex
index efdfe31eb..707854991 100644
--- a/test/elixir/lib/couch.ex
+++ b/test/elixir/lib/couch.ex
@@ -181,6 +181,7 @@ defmodule Couch do
     resp = Couch.post("/_session", body: %{:username => user, :password => pass})
 
     if expect == :success do
+      200 = resp.status_code
       true = resp.body["ok"]
       cookie = resp.headers[:"set-cookie"]
       [token | _] = String.split(cookie, ";")
diff --git a/test/elixir/test/config_test.exs b/test/elixir/test/config_test.exs
index ac7ec93e3..7c5db8a41 100644
--- a/test/elixir/test/config_test.exs
+++ b/test/elixir/test/config_test.exs
@@ -89,6 +89,8 @@ defmodule ConfigTest do
   test "Server-side password hashing, and raw updates disabling that", context do
     plain_pass = "s3cret"
     set_config(context, "admins", "administrator", plain_pass)
+    assert plain_pass == get_config(context, "admins", "administrator")
+    :timer.sleep(100)
     assert Couch.login("administrator", plain_pass)
     hash_pass = get_config(context, "admins", "administrator")