Permalink
Browse files

bcrypt and pbkdf2 hashing and verification working, needs more work i…

…nternally
  • Loading branch information...
1 parent 2d10809 commit 7e27268271f9e54764c1a94aa5cd8fe1c39cec8e @kudos committed Oct 23, 2012
Showing with 251 additions and 12 deletions.
  1. +85 −10 passwords/__init__.py
  2. +164 −0 passwords/pbkdf2.py
  3. +2 −2 setup.py
View
@@ -1,16 +1,91 @@
-import bcrypt
+__title__ = 'passwords'
+__version__ = '0.1.0'
-def hash(password, algorithm="bcrypt", cost=12):
- """
- Hash a password with a built-in salt using bcrypt
+import pbkdf2
+import os
+import base64
+from itertools import izip
+
+
+try:
+ import bcrypt
+except ImportError:
+ pass
+
+
+def hash(password, algorithm="pbkdf2", cost=8):
+ """
+ Hash a password
+
+ pbkdf2 sample:
+ $pbkdf2-256-1$8$FRakfnkgpMjnqs1Xxgjiwgycdf68be9b06451039cc\
+ 0f7075ec1c369fa36f055b1705ec7a
+ bcrypt sample:
+ $bcrypt-2a$08$9y1RbQ8Acdxq72Scf2ZqwuFSk9leg7Y.7E/lyYrDEjtN6kTIG4GKi
+
+ The returned strings are broken down into
+ - The algorithm and version used
+ - The cost factor, number of iterations over the hash
+ - The salt
+ - The password
"""
- salt = bcrypt.gensalt(cost);
- return bcrypt.hashpw(password, salt)
-
+ if algorithm == "bcrypt":
+ salt = bcrypt.gensalt(cost)
+ return bcrypt.hashpw(password, salt).replace("$2a$", "$bcrypt-2a$")
+
+ elif algorithm == "pbkdf2":
+ meta, salt = _generate_salt(cost)
+
+ for iteration in range(0, min(max(cost, 4), 31)):
+ password = pbkdf2.pbkdf2_hex(password, salt)
+
+ return meta + salt + password
+
+
def verify(password, hash):
"""
Verify a password against a passed hash
"""
- salt = hash[:29]
- return bcrypt.hashpw(password, salt) == hash
-
+ head, algorithm, cost, salthash = hash.split("$")
+
+ if algorithm == "pbkdf2-256-1":
+
+ salt = salthash[:24]
+
+ password_hash = salthash[24:]
+
+ rehash = password
+
+ for iteration in range(0, min(max(int(cost), 4), 31)):
+ rehash = pbkdf2.pbkdf2_hex(rehash, salt)
+
+ return _safe_str_cmp(rehash, password_hash)
+
+ elif algorithm == "bcrypt-2a":
+ # replace our algo identifier with bcrypt's internal one
+ # for some reason bcrypt couples the algo information with
+ # the salt rather than the completed hash
+ hash = hash.replace("$bcrypt-2a$", "$2a$")
+ salt = hash[:29]
+ return _safe_str_cmp(bcrypt.hashpw(password, salt), hash)
+
+
+def _safe_str_cmp(a, b):
+ """
+ Regular string compare will bail at the earliest opportunity
+ which allows timing attacks
+
+ Efficiently iterate over the hashes
+ """
+ if len(a) != len(b):
+ return False
+ rv = 0
+ for x, y in izip(a, b):
+ rv |= ord(x) ^ ord(y)
+ return rv == 0
+
+
+def _generate_salt(cost):
+ meta = "$pbkdf2-256-1$" + str(cost) + "$"
+ # using 18 instead of bcrypt's 16 because base64 is going to pad it anyway
+ return meta, base64.b64encode(os.urandom(18))
View
@@ -0,0 +1,164 @@
+# Copyright (c) 2011 by Armin Ronacher.
+#
+# Some rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+#
+# * The names of the contributors may not be used to endorse or
+# promote products derived from this software without specific
+# prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# -*- 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()
View
@@ -19,11 +19,11 @@
setup(
name='passwords',
- version='0.0.4',
+ version='0.0.5',
description='Passwords for everyone.',
author='Jonathan Cremin',
author_email='jonathan@crem.in',
- url='http://crem.in/passwords',
+ url='http://github.com/kudos/passwords',
packages=packages,
package_data={'': ['LICENSE']},
package_dir={'passwords': 'passwords'},

0 comments on commit 7e27268

Please sign in to comment.