Commits

Daniel Holth committed ad55d64

Add implementation based on Python's crypt module.

Comments (0)

Files changed (6)

+0.9
+===
+- Add cryptacular.crypt.CRYPTPasswordManager(prefix) based on Python's
+  builtin crypt(). Why didn't I think of this before?!
+
 0.5
 ===
 - use normal Python extension module instead of ctypes for bcrypt
 with a Python fallback when M2Crypto is not available. You can use this
 even if you cannot run C extension modules in your Python.
 
+cryptacular.crypt
+-----------------
+
+``cryptacular.crypt`` uses Python's builtin ``crypt`` module, available on
+Unix, to hash passwords. It takes a string such as '$1$' as an argument
+to determine which kind of hash the underlying ``crypt()`` function will
+produce (see ``man crypt`` for details). ``crypt()`` can even provide
+bcrypt hashes if you are lucky; the SHA hashes invented for RedHat are also
+good.
+
+On my Ubuntu system::
+
+    from cryptacular.crypt import CRYPTPasswordManager, SHA256CRYPT
+    manager = CRYPTPasswordManager(SHA256CRYPT)
+    manager.encode('secret')
+    >>> '$5$Ka9M/5GqJWMCnLI7$ZR0k9g2NlnXvgjjDYmobVUuLzfn/Tmo.vnW4WvW5Tx/'
+    manager.encode('secret')
+    >>> '$5$o4RUq2zuVWYWZpuq$35VyAVxfeL4sQ9//ODNw8jIDW7khJ5s0lUlXCHJ6WZ2'
+

cryptacular/core/__init__.py

+# -*- coding: utf-8 -*-
+#
 # Copyright (c) 2009 Daniel Holth <dholth@fastmail.fm>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy

cryptacular/crypt/__init__.py

+# -*- coding: utf-8 -*-
+"""
+Cryptacular password manager based on builtin ``crypt`` module (available
+on Unix). Available crypt functions will vary by system. See ``man crypt``.
+
+Usage::
+
+    try:
+        manager = CRYPTPasswordManager(cryptacular.crypt.SHA256CRYPT)
+        hashed = manager.encode('secret')
+        assert manager.check(hashed, 'secret') == True
+    except NotImplementedError:
+        print "SHA256CRYPT is not implemented on your system."
+"""
+# Copyright (c) 2011 Daniel Holth <dholth@fastmail.fm>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+__all__ = ['CRYPTPasswordManager', 'OLDCRYPT', 'MD5CRYPT', 'SHA256CRYPT',
+    'SHA512CRYPT', 'BCRYPT']
+
+import os
+import re
+import crypt
+import base64
+
+OLDCRYPT = ""
+BCRYPT = "$2a$"
+MD5CRYPT = "$1$"
+SHA256CRYPT = "$5$"
+SHA512CRYPT = "$6$"
+
+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()
+
+    def encode(self, password):
+        """Hash a password using the builtin crypt module."""
+        salt = self.PREFIX + base64.b64encode(os.urandom(12), altchars='./')
+        if isinstance(password, unicode):
+            password = password.encode('utf-8')
+        if not isinstance(password, str):
+            raise TypeError("password must be a str")
+        rc = self._crypt(password, salt)
+        return rc
+
+    def check(self, encoded, password):
+        """Check a bcrypt password hash against a password."""
+        if isinstance(password, unicode):
+            password = password.encode('utf-8')
+        if isinstance(encoded, unicode):
+            encoded = encoded.encode('utf-8')
+        if not isinstance(password, str):
+            raise TypeError("password must be a str")
+        if not isinstance(encoded, str):
+            raise TypeError("encoded must be a str")
+        if not self.match(encoded):
+            return False
+        rc = self._crypt(password, encoded)
+        return rc == encoded
+
+    def match(self, hash):
+        """Return True if hash starts with our prefix."""
+        return hash.startswith(self.PREFIX)
+

cryptacular/crypt/test_crypt.py

+from nose.tools import eq_, raises, assert_false, assert_true, assert_not_equal
+from cryptacular.crypt import *
+
+class TestCRYPTPasswordManager(object):
+    snowpass = u"hashy the \N{SNOWMAN}"
+
+    PREFIX = OLDCRYPT
+
+    def setup(self):
+        self.manager = CRYPTPasswordManager(self.PREFIX)
+
+    @raises(TypeError)
+    def test_None1(self):    
+        self.manager.encode(None)
+
+    @raises(TypeError)
+    def test_None2(self):    
+        self.manager.check(None, 'xyzzy')
+
+    @raises(TypeError)
+    def test_None3(self):
+        hash = self.manager.encode('xyzzy')
+        self.manager.check(hash, None)
+
+    def test_badhash(self):
+        eq_(self.manager.check('$p5k2$400$ZxK4ZBJCfQg=$kBpklVI9kA13kP32HMZL0rloQ1M=', self.snowpass), False)
+
+    def test_shorthash(self):
+        manager = self.manager
+        def match(hash):
+            return True
+        manager.match = match
+        short_hash = manager.encode(self.snowpass)[:11]
+        assert_true(manager.match(short_hash))
+        assert_false(manager.check(short_hash, self.snowpass))
+
+    def test_emptypass(self):
+        self.manager.encode('')
+
+    def test_general(self):
+        manager = self.manager
+        hash = manager.encode(self.snowpass)
+        eq_(manager.match(hash), True)
+        assert hash.startswith(self.PREFIX)
+        assert_true(manager.check(hash, self.snowpass))
+        password = "xyzzy"
+        hash = manager.encode(password)
+        assert_true(manager.check(hash, password))
+        assert_true(manager.check(unicode(hash), password))
+        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
+
+class TestCPM_MD5CRYPT(TestCRYPTPasswordManager):
+    PREFIX = SHA256CRYPT
+
+class TestCPM_MD5CRYPT(TestCRYPTPasswordManager):
+    PREFIX = SHA512CRYPT
+
+@raises(NotImplementedError)
+def test_bogocrypt():
+    CRYPTPasswordManager('$bogo$')
+
+@raises(NotImplementedError)
+def test_oddcrypt():
+    """crypt.crypt with empty prefix returns hash != 13 characters?"""
+    class BCPM(CRYPTPasswordManager):
+        _crypt = lambda x, y, z: '4' * 14
+    BCPM('') 
+
 requires = [ ]
 
 setup(name='cryptacular',
-      version='0.5.1',
+      version='0.9',
       description='A password hashing framework with bcrypt and pbkdf2.',
       long_description=README + '\n\n' +  CHANGES,
       classifiers=[
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 4 - Beta",
         "Intended Audience :: Developers",
         "Programming Language :: Python",
+        "Programming Language :: C",
         ],
       author='Daniel Holth',
       author_email='dholth@fastmail.fm',
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.