diff options
author | Javier Candeira <javier@candeira.com> | 2014-10-08 12:09:21 +1100 |
---|---|---|
committer | Robert Kowalski <robertkowalski@apache.org> | 2014-11-13 12:07:58 +0100 |
commit | 6de6ca673c082f8c2c093e76f2834407b1ab0bed (patch) | |
tree | 24cb98e3e1f9ebf85da9a7e7fccc71068d35fbcf /dev | |
parent | cf3b48e47f03dfb307b1139d2751a3580c6532c8 (diff) | |
download | couchdb-6de6ca673c082f8c2c093e76f2834407b1ab0bed.tar.gz |
fixes COUCHDB-2362 admin and cookie auth in dev cluster
Ensure that the cookie secret is the same on all nodes.
When running the dev cluster with dev/run and the --admin option,
ensure that the hashed admin password is the same on all nodes.
Includes a copy of Armin Ronacher's pbkdf2.py:
- https://github.com/mitsuhiko/python-pbkdf2/blob/master/pbkdf2.py
Diffstat (limited to 'dev')
-rw-r--r-- | dev/pbkdf2.py | 130 | ||||
-rwxr-xr-x | dev/run | 29 |
2 files changed, 158 insertions, 1 deletions
diff --git a/dev/pbkdf2.py b/dev/pbkdf2.py new file mode 100644 index 000000000..b7a7dd42e --- /dev/null +++ b/dev/pbkdf2.py @@ -0,0 +1,130 @@ +# -*- 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. +""" +import hmac +import hashlib +from struct import Struct +from operator import xor +from itertools import izip, starmap + + +_pack_int = Struct('>I').pack + + +def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): + """Like :func:`pbkdf2_bin` but returns a hex encoded string.""" + return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).encode('hex') + + +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(data, None, hashfunc) + def _pseudorandom(x, mac=mac): + h = mac.copy() + h.update(x) + return map(ord, h.digest()) + buf = [] + for block in xrange(1, -(-keylen // mac.digest_size) + 1): + rv = u = _pseudorandom(salt + _pack_int(block)) + for i in xrange(iterations - 1): + u = _pseudorandom(''.join(map(chr, u))) + rv = starmap(xor, izip(rv, u)) + buf.extend(rv) + 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 + print + 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() @@ -25,7 +25,12 @@ import sys import time import traceback import urllib +import uuid +from pbkdf2 import pbkdf2_hex + +# clipped down from e.g. '0x594fc30efe7746318d7d79684a15cfd0L' +COMMON_SALT = hex(uuid.uuid4().int)[2:-1] USAGE = "%prog [options] [command to run...]" DEV_PATH = os.path.dirname(os.path.abspath(__file__)) @@ -73,11 +78,33 @@ def hack_default_ini(opts, node, args, contents): return contents +def hashify(pwd, salt=COMMON_SALT): + """ + Implements password hasshing according to: + - https://issues.apache.org/jira/browse/COUCHDB-1060 + - https://issues.apache.org/jira/secure/attachment/12492631/0001-Integrate-PBKDF2.patch + + This test uses 'candeira:candeira' + + >>> hashify(candeira) + -pbkdf2-99eb34d97cdaa581e6ba7b5386e112c265c5c670,d1d2d4d8909c82c81b6c8184429a0739,10 + """ + iterations = 10 + keylen = 20 + derived_key = pbkdf2_hex(pwd, salt, iterations, keylen) + return "-pbkdf2-%s,%s,%s" % (derived_key, salt, iterations) + def hack_local_ini(opts, node, args, contents): + # make sure all three nodes have the same secret + secret_line = "secret = %s\n" % COMMON_SALT + previous_line = "; require_valid_user = false\n" + contents = contents.replace(previous_line, previous_line + secret_line) + # if --admin user:password on invocation, make sure all three nodes + # have the same hashed password if opts.admin is None: return contents usr, pwd = opts.admin.split(":", 1) - return contents + "\n[admins]\n%s = %s" % (usr, pwd) + return contents + "\n%s = %s" % (usr, hashify(pwd)) def write_config(opts, node, args): |