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")