[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

authorSylvain Thénault <sylvain.thenault@logilab.fr>
changeset9c59258e7798
branchstable
phasepublic
hiddenno
parent revision#2279e02e62be Added tag cubicweb-debian-version-3.14.6-1 for changeset 75364c099490
child revision#f6d455b9346f [manage / default index views] discard login/logout templates
files modified by this revision
__pkginfo__.py
debian/control
md5crypt.py
server/sources/native.py
server/test/unittest_querier.py
server/utils.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1331917188 -3600
# Fri Mar 16 17:59:48 2012 +0100
# Branch stable
# 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 --git a/__pkginfo__.py b/__pkginfo__.py
@@ -52,10 +52,11 @@
1      'Twisted': '',
2      # XXX graphviz
3      # server dependencies
4      'logilab-database': '>= 1.8.1',
5      'pysqlite': '>= 2.5.5', # XXX install pysqlite2
6 +    'passlib': '',
7      }
8 
9  __recommends__ = {
10      'Pyro': '>= 3.9.1, < 4.0.0',
11      'PIL': '',                  # for captcha
diff --git a/debian/control b/debian/control
@@ -33,11 +33,11 @@
12  Architecture: all
13  XB-Python-Version: ${python:Versions}
14  Conflicts: cubicweb-multisources
15  Replaces: cubicweb-multisources
16  Provides: cubicweb-multisources
17 -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
18 +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
19  Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
20  Description: server part of the CubicWeb framework
21   CubicWeb is a semantic web application framework.
22   .
23   This package provides the repository server part of the system.
diff --git a/md5crypt.py b/md5crypt.py
@@ -49,22 +49,20 @@
24          n = n - 1
25          ret = ret + ITOA64[v & 0x3f]
26          v = v >> 6
27      return ret
28 
29 -def crypt(pw, salt, magic=None):
30 +def crypt(pw, salt):
31      if isinstance(pw, unicode):
32          pw = pw.encode('utf-8')
33 -    if magic is None:
34 -        magic = MAGIC
35      # Take care of the magic string if present
36 -    if salt[:len(magic)] == magic:
37 -        salt = salt[len(magic):]
38 +    if salt.startswith(MAGIC):
39 +        salt = salt[len(MAGIC):]
40      # salt can have up to 8 characters:
41      salt = salt.split('$', 1)[0]
42      salt = salt[:8]
43 -    ctx = pw + magic + salt
44 +    ctx = pw + MAGIC + salt
45      final = md5(pw + salt + pw).digest()
46      for pl in xrange(len(pw), 0, -16):
47          if pl > 16:
48              ctx = ctx + final[:16]
49          else:
@@ -112,6 +110,6 @@
50                             |(int(ord(final[15]))), 4)
51      passwd = passwd + to64((int(ord(final[4])) << 16)
52                             |(int(ord(final[10])) << 8)
53                             |(int(ord(final[5]))), 4)
54      passwd = passwd + to64((int(ord(final[11]))), 2)
55 -    return salt + '$' + passwd
56 +    return passwd
diff --git a/server/sources/native.py b/server/sources/native.py
@@ -1588,11 +1588,11 @@
57                  raise AuthenticationError('bad login')
58              if pwd is None:
59                  # if pwd is None but a password is provided, something is wrong
60                  raise AuthenticationError('bad password')
61              # passwords are stored using the Bytes type, so we get a StringIO
62 -            args['pwd'] = Binary(crypt_password(password, pwd.getvalue()[:2]))
63 +            args['pwd'] = Binary(crypt_password(password, pwd.getvalue()))
64          # get eid from login and (crypted) password
65          rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
66          try:
67              return rset[0][0]
68          except IndexError:
diff --git a/server/test/unittest_querier.py b/server/test/unittest_querier.py
@@ -1257,11 +1257,11 @@
69                            self.execute, "Any P WHERE X is CWUser, X login 'bob', X upassword P")
70          cursor = self.cnxset['system']
71          cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
72                         % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
73          passwd = str(cursor.fetchone()[0])
74 -        self.assertEqual(passwd, crypt_password('toto', passwd[:2]))
75 +        self.assertEqual(passwd, crypt_password('toto', passwd))
76          rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
77                              {'pwd': Binary(passwd)})
78          self.assertEqual(len(rset.rows), 1)
79          self.assertEqual(rset.description, [('CWUser',)])
80 
@@ -1272,11 +1272,11 @@
81                              {'pwd': 'tutu'})
82          cursor = self.cnxset['system']
83          cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
84                         % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
85          passwd = str(cursor.fetchone()[0])
86 -        self.assertEqual(passwd, crypt_password('tutu', passwd[:2]))
87 +        self.assertEqual(passwd, crypt_password('tutu', passwd))
88          rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
89                              {'pwd': Binary(passwd)})
90          self.assertEqual(len(rset.rows), 1)
91          self.assertEqual(rset.description, [('CWUser',)])
92 
diff --git a/server/utils.py b/server/utils.py
@@ -26,31 +26,53 @@
93  from getpass import getpass
94  from random import choice
95 
96  from cubicweb.server import SOURCE_TYPES
97 
98 -try:
99 -    from crypt import crypt
100 -except ImportError:
101 -    # crypt is not available (eg windows)
102 -    from cubicweb.md5crypt import crypt
103 +from passlib.utils import handlers as uh, to_hash_str
104 +from passlib.context import CryptContext
105 +
106 +from cubicweb.md5crypt import crypt as md5crypt
107 
108 
109 -def getsalt(chars=string.letters + string.digits):
110 -    """generate a random 2-character 'salt'"""
111 -    return choice(chars) + choice(chars)
112 +class CustomMD5Crypt(uh.HasSalt, uh.GenericHandler):
113 +    name = 'cubicweb-md5crypt'
114 +    setting_kwds = ("salt",)
115 +    min_salt_size = 0
116 +    max_salt_size = 8
117 +    salt_chars = uh.H64_CHARS
118 
119 +    @classmethod
120 +    def from_string(cls, hash):
121 +        if hash is None:
122 +            raise ValueError("no hash specified")
123 +        if hash.count('$') != 1:
124 +            raise ValueError("invalid cubicweb-md5 hash")
125 +        salt = hash.split('$', 1)[0]
126 +        chk = hash.split('$', 1)[1]
127 +        return cls(salt=salt, checksum=chk, strict=True)
128 +
129 +    def to_string(self):
130 +        return to_hash_str(u'%s$%s' % (self.salt, self.checksum or u''))
131 +
132 +    def calc_checksum(self, secret):
133 +        return md5crypt(secret, self.salt.encode('ascii'))
134 +
135 +myctx = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt'])
136 
137  def crypt_password(passwd, salt=None):
138      """return the encrypted password using the given salt or a generated one
139      """
140 -    if passwd is None:
141 -        return None
142      if salt is None:
143 -        salt = getsalt()
144 -    return crypt(passwd, salt)
145 -
146 +        return myctx.encrypt(passwd)
147 +    # empty hash, accept any password for backwards compat
148 +    if salt == '':
149 +        return salt
150 +    if myctx.verify(passwd, salt):
151 +        return salt
152 +    # wrong password
153 +    return ''
154 
155  def cartesian_product(seqin):
156      """returns a generator which returns the cartesian product of `seqin`
157 
158      for more details, see :