[security] use a stronger encryption algorythm for password, keeping bw compat stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 16 Mar 2012 17:59:48 +0100
branchstable
changeset 8317 9c59258e7798
parent 8305 2279e02e62be
child 8319 f6d455b9346f
[security] use a stronger encryption algorythm for password, keeping bw compat Administrator should ask their users to reenter new password so they benefit from the new encryption. Also, new encryption is cross-platform compatible, eg you may now move an instance from windows to linux painlessly
__pkginfo__.py
debian/control
md5crypt.py
server/sources/native.py
server/test/unittest_querier.py
server/utils.py
--- a/__pkginfo__.py	Tue Mar 13 15:27:30 2012 +0100
+++ b/__pkginfo__.py	Fri Mar 16 17:59:48 2012 +0100
@@ -54,6 +54,7 @@
     # server dependencies
     'logilab-database': '>= 1.8.1',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
+    'passlib': '',
     }
 
 __recommends__ = {
--- a/debian/control	Tue Mar 13 15:27:30 2012 +0100
+++ b/debian/control	Fri Mar 16 17:59:48 2012 +0100
@@ -35,7 +35,7 @@
 Conflicts: cubicweb-multisources
 Replaces: cubicweb-multisources
 Provides: cubicweb-multisources
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.1), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.1), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, python-passlib
 Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
--- a/md5crypt.py	Tue Mar 13 15:27:30 2012 +0100
+++ b/md5crypt.py	Fri Mar 16 17:59:48 2012 +0100
@@ -51,18 +51,16 @@
         v = v >> 6
     return ret
 
-def crypt(pw, salt, magic=None):
+def crypt(pw, salt):
     if isinstance(pw, unicode):
         pw = pw.encode('utf-8')
-    if magic is None:
-        magic = MAGIC
     # Take care of the magic string if present
-    if salt[:len(magic)] == magic:
-        salt = salt[len(magic):]
+    if salt.startswith(MAGIC):
+        salt = salt[len(MAGIC):]
     # salt can have up to 8 characters:
     salt = salt.split('$', 1)[0]
     salt = salt[:8]
-    ctx = pw + magic + salt
+    ctx = pw + MAGIC + salt
     final = md5(pw + salt + pw).digest()
     for pl in xrange(len(pw), 0, -16):
         if pl > 16:
@@ -114,4 +112,4 @@
                            |(int(ord(final[10])) << 8)
                            |(int(ord(final[5]))), 4)
     passwd = passwd + to64((int(ord(final[11]))), 2)
-    return salt + '$' + passwd
+    return passwd
--- a/server/sources/native.py	Tue Mar 13 15:27:30 2012 +0100
+++ b/server/sources/native.py	Fri Mar 16 17:59:48 2012 +0100
@@ -1590,7 +1590,7 @@
                 # if pwd is None but a password is provided, something is wrong
                 raise AuthenticationError('bad password')
             # passwords are stored using the Bytes type, so we get a StringIO
-            args['pwd'] = Binary(crypt_password(password, pwd.getvalue()[:2]))
+            args['pwd'] = Binary(crypt_password(password, pwd.getvalue()))
         # get eid from login and (crypted) password
         rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
         try:
--- a/server/test/unittest_querier.py	Tue Mar 13 15:27:30 2012 +0100
+++ b/server/test/unittest_querier.py	Fri Mar 16 17:59:48 2012 +0100
@@ -1259,7 +1259,7 @@
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
         passwd = str(cursor.fetchone()[0])
-        self.assertEqual(passwd, crypt_password('toto', passwd[:2]))
+        self.assertEqual(passwd, crypt_password('toto', passwd))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
         self.assertEqual(len(rset.rows), 1)
@@ -1274,7 +1274,7 @@
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
         passwd = str(cursor.fetchone()[0])
-        self.assertEqual(passwd, crypt_password('tutu', passwd[:2]))
+        self.assertEqual(passwd, crypt_password('tutu', passwd))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
         self.assertEqual(len(rset.rows), 1)
--- a/server/utils.py	Tue Mar 13 15:27:30 2012 +0100
+++ b/server/utils.py	Fri Mar 16 17:59:48 2012 +0100
@@ -28,27 +28,49 @@
 
 from cubicweb.server import SOURCE_TYPES
 
-try:
-    from crypt import crypt
-except ImportError:
-    # crypt is not available (eg windows)
-    from cubicweb.md5crypt import crypt
+from passlib.utils import handlers as uh, to_hash_str
+from passlib.context import CryptContext
+
+from cubicweb.md5crypt import crypt as md5crypt
 
 
-def getsalt(chars=string.letters + string.digits):
-    """generate a random 2-character 'salt'"""
-    return choice(chars) + choice(chars)
+class CustomMD5Crypt(uh.HasSalt, uh.GenericHandler):
+    name = 'cubicweb-md5crypt'
+    setting_kwds = ("salt",)
+    min_salt_size = 0
+    max_salt_size = 8
+    salt_chars = uh.H64_CHARS
 
+    @classmethod
+    def from_string(cls, hash):
+        if hash is None:
+            raise ValueError("no hash specified")
+        if hash.count('$') != 1:
+            raise ValueError("invalid cubicweb-md5 hash")
+        salt = hash.split('$', 1)[0]
+        chk = hash.split('$', 1)[1]
+        return cls(salt=salt, checksum=chk, strict=True)
+
+    def to_string(self):
+        return to_hash_str(u'%s$%s' % (self.salt, self.checksum or u''))
+
+    def calc_checksum(self, secret):
+        return md5crypt(secret, self.salt.encode('ascii'))
+
+myctx = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt'])
 
 def crypt_password(passwd, salt=None):
     """return the encrypted password using the given salt or a generated one
     """
-    if passwd is None:
-        return None
     if salt is None:
-        salt = getsalt()
-    return crypt(passwd, salt)
-
+        return myctx.encrypt(passwd)
+    # empty hash, accept any password for backwards compat
+    if salt == '':
+        return salt
+    if myctx.verify(passwd, salt):
+        return salt
+    # wrong password
+    return ''
 
 def cartesian_product(seqin):
     """returns a generator which returns the cartesian product of `seqin`