Commit 68beaf36 authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[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

--HG--
branch : stable
parent 2279e02e62be
......@@ -54,6 +54,7 @@ __depends__ = {
# server dependencies
'logilab-database': '>= 1.8.1',
'pysqlite': '>= 2.5.5', # XXX install pysqlite2
'passlib': '',
}
__recommends__ = {
......
......@@ -35,7 +35,7 @@ XB-Python-Version: ${python:Versions}
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.
......
......@@ -51,18 +51,16 @@ def to64 (v, n):
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 @@ def crypt(pw, salt, magic=None):
|(int(ord(final[10])) << 8)
|(int(ord(final[5]))), 4)
passwd = passwd + to64((int(ord(final[11]))), 2)
return salt + '$' + passwd
return passwd
......@@ -1590,7 +1590,7 @@ class LoginPasswordAuthentifier(BaseAuthentifier):
# 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:
......
......@@ -1259,7 +1259,7 @@ Any P1,B,E WHERE P1 identity P2 WITH
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 @@ Any P1,B,E WHERE P1 identity P2 WITH
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)
......
......@@ -28,27 +28,49 @@ from random import choice
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`
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment