[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
--- 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`