Commits

Daniel Holth committed 1be3347 Merge

merge

Comments (0)

Files changed (9)

 - Update to crypt_blowfish 1.2 (which fixes CVE-2011-2483, 8-bit character
   encoding vulterability. See http://www.openwall.com/crypt/ for details.)
 
+1.1
+===
+
+- Add `rounds` option to the `encode` methods of the bcrypt and pbkdf2
+  password managers which can be used to specify the number of rounds
+  (or the work factor in the case of bcrypt).
+
 1.0
 ===
 - Change version to 1.0
+[buildout]
+develop = .
+parts = test py
+versions = versions
+
+[test]
+recipe = zc.recipe.egg
+eggs = cryptacular [test]
+       nose
+
+[py]
+recipe = zc.recipe.egg
+eggs = cryptacular [test]
+interpreter = py
+
+[versions]

cryptacular/bcrypt/__init__.py

 
     SCHEME = "BCRYPT"
     PREFIX = "$2a$"
+    ROUNDS = 10
 
     _bcrypt_syntax = re.compile('\$2a\$[0-9]{2}\$[./A-Za-z0-9]{53}')
 
-    def encode(self, password):
+    def encode(self, password, rounds=None):
         """Hash a password using bcrypt.
 
         Note: only the first 72 characters of password are significant.
         """
-        settings = crypt_gensalt_rn('$2a$', 10, os.urandom(16))
+        work_factor = rounds or self.ROUNDS
+        settings = crypt_gensalt_rn('$2a$', work_factor, os.urandom(16))
         if settings is None:
             raise ValueError("_bcrypt.crypt_gensalt_rn returned None") # pragma NO COVERAGE
         if isinstance(password, unicode):
         if not isinstance(password, str):
             raise TypeError("password must be a str")
         rc = crypt_rn(password, settings)
-        if rc is None: 
+        if rc is None:
             raise ValueError("_bcrypt.crypt_rn returned None") # pragma NO COVERAGE
         return rc
 

cryptacular/bcrypt/test_bcrypt.py

         self.manager = BCRYPTPasswordManager()
 
     @raises(TypeError)
-    def test_None1(self):    
+    def test_None1(self):
         self.manager.encode(None)
 
     @raises(TypeError)
-    def test_None2(self):    
+    def test_None2(self):
         self.manager.check(None, 'xyzzy')
 
     @raises(TypeError)
         assert_true(manager.match(short_hash))
         manager.check(short_hash, self.snowpass)
 
+    @raises(ValueError)
+    def test_too_few_rounds(self):
+        self.manager.encode(self.snowpass, rounds=1)
+
+    @raises(ValueError)
+    def test_too_many_rounds(self):
+        self.manager.encode(self.snowpass, rounds=100)
+
     def test_emptypass(self):
         self.manager.encode('')
 
         assert_true(manager.check(unicode(hash), password))
         assert_false(manager.check(password, password))
         assert_not_equal(manager.encode(password), manager.encode(password))
+        hash = manager.encode(password, rounds=4)
+        assert_true(manager.check(hash, password))

cryptacular/crypt/__init__.py

 # THE SOFTWARE.
 
 __all__ = ['CRYPTPasswordManager', 'OLDCRYPT', 'MD5CRYPT', 'SHA256CRYPT',
-    'SHA512CRYPT', 'BCRYPT']
+    'SHA512CRYPT', 'BCRYPT', 'available']
 
 import os
 import re
 SHA256CRYPT = "$5$"
 SHA512CRYPT = "$6$"
 
+def available(prefix, _crypt=crypt.crypt):
+    # Lame 'is implemented' check.
+    l = len(_crypt('implemented?', prefix + 'xyzzy'))
+    if prefix == OLDCRYPT:
+        if l != 13:
+           return False
+    elif l < 26:
+        return False
+    return True
+
 class CRYPTPasswordManager(object):
     _crypt = crypt.crypt
     def __init__(self, prefix):
         """prefix: $1$ etc. indicating hashing scheme."""
         self.PREFIX = prefix
-        # Lame 'is implemented' check.
-        l = len(self._crypt('implemented?', prefix + 'xyzzy'))
-        if prefix == OLDCRYPT:
-            if l != 13:
-                raise NotImplementedError()
-        elif l < 26:
-            raise NotImplementedError()
+        if not available(prefix, self._crypt):
+            raise NotImplementedError
 
     def encode(self, password):
         """Hash a password using the builtin crypt module."""

cryptacular/crypt/test_crypt.py

         self.manager = CRYPTPasswordManager(self.PREFIX)
 
     @raises(TypeError)
-    def test_None1(self):    
+    def test_None1(self):
         self.manager.encode(None)
 
     @raises(TypeError)
-    def test_None2(self):    
+    def test_None2(self):
         self.manager.check(None, 'xyzzy')
 
     @raises(TypeError)
         assert_false(manager.check(password, password))
         assert_not_equal(manager.encode(password), manager.encode(password))
 
-class TestCPM_MD5CRYPT(TestCRYPTPasswordManager):
-    PREFIX = BCRYPT
 
-class TestCPM_MD5CRYPT(TestCRYPTPasswordManager):
-    PREFIX = MD5CRYPT
+if available(BCRYPT):
+    class TestCPM_BCRYPT(TestCRYPTPasswordManager):
+        PREFIX = BCRYPT
 
-class TestCPM_MD5CRYPT(TestCRYPTPasswordManager):
-    PREFIX = SHA256CRYPT
+if available(MD5CRYPT):
+    class TestCPM_MD5CRYPT(TestCRYPTPasswordManager):
+        PREFIX = MD5CRYPT
 
-class TestCPM_MD5CRYPT(TestCRYPTPasswordManager):
-    PREFIX = SHA512CRYPT
+if available(SHA256CRYPT):
+    class TestCPM_SHA256CRYPT(TestCRYPTPasswordManager):
+        PREFIX = SHA256CRYPT
+
+if available(SHA512CRYPT):
+    class TestCPM_SHA512CRYPT(TestCRYPTPasswordManager):
+        PREFIX = SHA512CRYPT
 
 @raises(NotImplementedError)
 def test_bogocrypt():
     """crypt.crypt with empty prefix returns hash != 13 characters?"""
     class BCPM(CRYPTPasswordManager):
         _crypt = lambda x, y, z: '4' * 14
-    BCPM('') 
-
+    BCPM('')

cryptacular/pbkdf2/__init__.py

 
     SCHEME = "PBKDF2"
     PREFIX = "$p5k2$"
+    ROUNDS = 1<<12
 
-    def encode(self, password, salt=None, iter=1<<12, keylen=20):
+    def encode(self, password, salt=None, rounds=None, keylen=20):
         if salt is None:
             salt = os.urandom(16)
+        rounds = rounds or self.ROUNDS
         if isinstance(password, unicode):
             password = password.encode("utf-8")
-        key = _pbkdf2(password, salt, iter, keylen)
+        key = _pbkdf2(password, salt, rounds, keylen)
         hash = "%s%x$%s$%s" % (
-                self.PREFIX, 
-                iter, 
+                self.PREFIX,
+                rounds,
                 urlsafe_b64encode(salt),
                 urlsafe_b64encode(key))
         return hash

cryptacular/pbkdf2/test_pbkdf2.py

     ret = pbkdf2( password, salt, itercount, keylen )
     hexret = ' '.join(map(lambda c: '%02x' % ord(c), ret)).upper()
     eq_(hexret, "6A 89 70 BF 68 C9 2C AE A8 4A 8D F2 85 10 85 86")
-    
+
     # from botan
     password = unhexlify('6561696D72627A70636F706275736171746B6D77')
     expect = 'C9A0B2622F13916036E29E7462E206E8BA5B50CE9212752EB8EA2A4AA7B40A4CC1BF'
     manager = PBKDF2PasswordManager()
     # Never call .encode with a salt.
     salt = urlsafe_b64decode('ZxK4ZBJCfQg=')
-    hash = manager.encode(u"hashy the \N{SNOWMAN}", salt)
+    text = u"hashy the \N{SNOWMAN}"
+    hash = manager.encode(text, salt)
     eq_(hash, '$p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0=')
     password = "xyzzy"
     hash = manager.encode(password)
     assert manager.check(unicode(hash), password)
     assert not manager.check(password, password)
     assert_not_equal(manager.encode(password), manager.encode(password))
+    hash = manager.encode(text, salt, rounds=1)
+    eq_(hash, "$p5k2$1$ZxK4ZBJCfQg=$Kexp0NAVgxlDwoA-TS34o8o2Okg=")
+    assert manager.check(hash, text)
 
 @raises(ValueError)
 def test_xorstr():
     xorstr('foo', 'quux')
 
-if __name__ == "__main__": 
+if __name__ == "__main__":
     test() # pragma: NO COVERAGE
 README = open(os.path.join(here, 'README.txt')).read()
 CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
 
-requires = [ ]
+tests_require = ["nose", "coverage"]
 
 setup(name='cryptacular',
       version='1.2',
       include_package_data=True,
       zip_safe=False,
       install_requires = ['setuptools'],
-      tests_require = requires,
+      tests_require = tests_require,
+      extras_require = dict(test=tests_require),
       test_suite = 'nose.collector',
       ext_modules=[
           Extension('cryptacular.bcrypt._bcrypt',