# HG changeset patch # User Sylvain Thénault # Date 1331917188 -3600 # Node ID 9c59258e7798c29b55285cd83afbb50279350690 # Parent 2279e02e62be26872fcb04ce62d93a3de6531738 [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 diff -r 2279e02e62be -r 9c59258e7798 __pkginfo__.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__ = { diff -r 2279e02e62be -r 9c59258e7798 debian/control --- 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. diff -r 2279e02e62be -r 9c59258e7798 md5crypt.py --- 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 diff -r 2279e02e62be -r 9c59258e7798 server/sources/native.py --- 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: diff -r 2279e02e62be -r 9c59258e7798 server/test/unittest_querier.py --- 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) diff -r 2279e02e62be -r 9c59258e7798 server/utils.py --- 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`