# HG changeset patch # User Julien Cristau # Date 1347283030 -7200 # Node ID 3d2038d6f20dea3b6f3cfa5211db2ed93fb26104 # Parent eb7a171cec72b56c235edeb2f18227bb9399c114 [sources/native] automatically update passwords using deprecated hashes on login Closes #2465904 diff -r eb7a171cec72 -r 3d2038d6f20d server/sources/native.py --- a/server/sources/native.py Fri Sep 14 17:42:24 2012 +0200 +++ b/server/sources/native.py Mon Sep 10 15:17:10 2012 +0200 @@ -61,7 +61,7 @@ from cubicweb.schema import VIRTUAL_RTYPES from cubicweb.cwconfig import CubicWebNoAppConfiguration from cubicweb.server import hook -from cubicweb.server.utils import crypt_password, eschema_eid +from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn from cubicweb.server.rqlannotation import set_qdata from cubicweb.server.hook import CleanupDeletedEidsCacheOp @@ -1629,7 +1629,22 @@ # get eid from login and (crypted) password rset = self.source.syntax_tree_search(session, self._auth_rqlst, args) try: - return rset[0][0] + user = rset[0][0] + # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used + # before 3.14.7), update with a fresh one + if pwd.getvalue(): + verify, newhash = verify_and_update(password, pwd.getvalue()) + if not verify: # should not happen, but... + raise AuthenticationError('bad password') + if newhash: + session.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s" % ( + SQL_PREFIX + 'CWUser', + SQL_PREFIX + 'upassword', + SQL_PREFIX + 'login'), + {'newhash': self.source._binary(newhash), + 'login': login}) + session.commit(free_cnxset=False) + return user except IndexError: raise AuthenticationError('bad password') diff -r eb7a171cec72 -r 3d2038d6f20d server/test/unittest_security.py --- a/server/test/unittest_security.py Fri Sep 14 17:42:24 2012 +0200 +++ b/server/test/unittest_security.py Mon Sep 10 15:17:10 2012 +0200 @@ -25,9 +25,10 @@ from rql import RQLException from cubicweb.devtools.testlib import CubicWebTC -from cubicweb import Unauthorized, ValidationError, QueryError +from cubicweb import Unauthorized, ValidationError, QueryError, Binary from cubicweb.schema import ERQLExpression from cubicweb.server.querier import check_read_access +from cubicweb.server.utils import _CRYPTO_CTX class BaseSecurityTC(CubicWebTC): @@ -35,7 +36,8 @@ def setup_database(self): super(BaseSecurityTC, self).setup_database() self.create_user(self.request(), 'iaminusersgrouponly') - + hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt') + self.create_user(self.request(), 'oldpassword', password=Binary(hash)) class LowLevelSecurityFunctionTC(BaseSecurityTC): @@ -60,6 +62,18 @@ self.assertRaises(Unauthorized, cu.execute, 'Any X,P WHERE X is CWUser, X upassword P') + def test_update_password(self): + """Ensure that if a user's password is stored with a deprecated hash, it will be updated on next login""" + oldhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0]) + with self.login('oldpassword') as cu: + pass + newhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0]) + self.assertNotEqual(oldhash, newhash) + self.assertTrue(newhash.startswith('$6$')) + with self.login('oldpassword') as cu: + pass + self.assertEqual(newhash, str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])) + class SecurityRewritingTC(BaseSecurityTC): def hijack_source_execute(self): diff -r eb7a171cec72 -r 3d2038d6f20d server/utils.py --- a/server/utils.py Fri Sep 14 17:42:24 2012 +0200 +++ b/server/utils.py Mon Sep 10 15:17:10 2012 +0200 @@ -52,7 +52,9 @@ return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8') _calc_checksum = calc_checksum -_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1']) +_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'], + deprecated=['cubicwebmd5crypt', 'des_crypt']) +verify_and_update = _CRYPTO_CTX.verify_and_update def crypt_password(passwd, salt=None): """return the encrypted password using the given salt or a generated one