Ticket #4398: IUsernamePassword_over_pb2.patch

File IUsernamePassword_over_pb2.patch, 18.2 KB (added by Louis, 9 years ago)

Patch allowing to use custom hash method for PB authentication. Improves IUsernamePassword_over_pb.patch

  • test/test_pb.py

     
    1212# Clean up warning suppression.
    1313
    1414import sys, os, time, gc, weakref
     15import hashlib, crypt
    1516
    1617from cStringIO import StringIO
    1718from zope.interface import implements, Interface
    1819
    1920from twisted.trial import unittest
    2021from twisted.spread import pb, util, publish, jelly
    21 from twisted.internet import protocol, main, reactor
     22from twisted.internet import protocol, main, reactor, defer
    2223from twisted.internet.error import ConnectionRefusedError
    2324from twisted.internet.defer import Deferred, gatherResults, succeed
    2425from twisted.protocols.policies import WrappingFactory
     
    11661167        return (pb.IPerspective, persp, lambda : (mind, persp.logout()))
    11671168
    11681169
     1170class InMemoryUsernameMD5PasswordDatabaseDontUse(checkers.InMemoryUsernamePasswordDatabaseDontUse):
     1171    """
     1172    Checker used to test L{spread.pb.IUsernameMD5Password}.
    11691173
     1174    It is identical to L{checkers.InMemoryUsernamePasswordDatabaseDontUse},
     1175    excepted that password is stored hashed (md5, no salt).
     1176    """
     1177    implements(checkers.ICredentialsChecker)
     1178    credentialInterfaces = (
     1179        pb.IUsernameMD5Password,
     1180    )
     1181    def requestAvatarId(self, credentials):
     1182        if credentials.username in self.users:
     1183            return defer.maybeDeferred(
     1184                credentials.checkMD5Password,
     1185                self.users[credentials.username]).addCallback(
     1186                self._cbPasswordMatch, str(credentials.username))
     1187        else:
     1188            return defer.fail(error.UnauthorizedLogin())
     1189
     1190def dummyHash(secret, salt):
     1191    return secret + salt
     1192def dummyGetSalt(username, hashed_password):
     1193    return str(len(username))
     1194
     1195class HashMethodCredential(pb.UsernameHashPassword):
     1196    """A dummy credential."""
     1197    def pwHashMethod(self, secret, salt):
     1198        return dummyHash(secret, salt)
     1199
     1200class DummyChallengerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse):
     1201    """
     1202    A checker used to test different hash methods.
     1203
     1204    It takes as argument the method used to retrieve the salt, given the
     1205    username and hashed password.
     1206    """
     1207    implements(checkers.IChallenger)
     1208
     1209    def __init__(self, getSalt, *args, **kwargs):
     1210        self._getsalt = getSalt
     1211        checkers.InMemoryUsernamePasswordDatabaseDontUse.__init__(self,
     1212                *args, **kwargs)
     1213    def challengeHashMethod(self, challenge, data):
     1214        """
     1215        Hash method used for the challenge part of authentication: we use
     1216        the default one used by PB.
     1217        """
     1218        return pb.challengeHash(challenge, data)
     1219    def getSalt(self, username):
     1220        """
     1221        Return the salt used to hash the password of the given username.
     1222        """
     1223        return self._getsalt(username, self.users[username])
     1224
     1225class DummyCredentialWithBadHashMethod(pb.UsernameHashPassword):
     1226    """Another dummy credential."""
     1227    def pwHashMethod(self, secret, salt):
     1228        return "salt"
     1229
    11701230class NewCredLeakTests(unittest.TestCase):
    11711231    """
    11721232    Tests to try to trigger memory leaks.
     
    13751435
    13761436    def test_loginLogout(self):
    13771437        """
    1378         Test that login can be performed with IUsernamePassword credentials and
     1438        Test that login can be performed with L{cred.credentials.IUsernamePassword} credentials and
    13791439        that when the connection is dropped the avatar is logged out.
    13801440        """
    13811441        self.portal.registerChecker(
     
    14051465        self.addCleanup(connector.disconnect)
    14061466        return d
    14071467
     1468    def test_loginLogoutWithLegacyMD5Hash(self):
     1469        """
     1470        Test that login can be performed with legacy
     1471        L{spread.pb.IUsernameMD5Password} credentials and that when the
     1472        connection is dropped the avatar is logged out.
     1473        """
     1474        self.portal.registerChecker(
     1475            InMemoryUsernameMD5PasswordDatabaseDontUse(LOGIN=hashlib.md5("PASSWORD").digest()))
     1476        factory = pb.PBClientFactory()
    14081477
     1478        mind = "BRAINS!"
     1479
     1480        d = factory.login(
     1481                credentials.UsernamePassword("LOGIN", "PASSWORD"),
     1482                mind)
     1483        def cbLogin(perspective):
     1484            self.assertTrue(self.realm.lastPerspective.loggedIn)
     1485            self.assertIsInstance(perspective, pb.RemoteReference)
     1486            return self._disconnect(None, factory)
     1487        d.addCallback(cbLogin)
     1488
     1489        def cbLogout(ignored):
     1490            self.assertTrue(self.realm.lastPerspective.loggedOut)
     1491        d.addCallback(cbLogout)
     1492
     1493        connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
     1494        self.addCleanup(connector.disconnect)
     1495        return d
     1496
     1497    def genericTest_loginLogoutWithIHashMethod(self, checker, credential):
     1498        """
     1499        Test that login can be performed with
     1500        L{cred.credentials.IUsernamePassword} and L{spread.pb.IHashMethod}
     1501        credentials, and an implementation of the L{cred.checkers.IChallenger}
     1502        checker, and that when the connection is dropped the avatar is logged
     1503        out.
     1504
     1505        It takes as arguments a checker and a credential, which are to be
     1506        tested.
     1507        """
     1508        self.portal.registerChecker(checker)
     1509        factory = pb.PBClientFactory()
     1510
     1511        mind = "BRAINS!"
     1512
     1513        d = factory.login(credential, mind)
     1514        def cbLogin(perspective):
     1515            self.assertTrue(self.realm.lastPerspective.loggedIn)
     1516            self.assertIsInstance(perspective, pb.RemoteReference)
     1517            return self._disconnect(None, factory)
     1518        d.addCallback(cbLogin)
     1519
     1520        def cbLogout(ignored):
     1521            self.assertTrue(self.realm.lastPerspective.loggedOut)
     1522        d.addCallback(cbLogout)
     1523
     1524        connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
     1525        self.addCleanup(connector.disconnect)
     1526        return d
     1527
     1528    def test_loginLogoutWithHashlibIHashMethod(self):
     1529        """
     1530        Test of the L{pb.UsernameHashlibPassword} credentials with a
     1531        L{cred.checkers.IChallenger} checker.
     1532        """
     1533        return self.genericTest_loginLogoutWithIHashMethod(
     1534                checker = DummyChallengerChecker(lambda u, p:"",
     1535                    LOGIN = hashlib.sha384("PASSWORD").digest()),
     1536                credential = pb.UsernameHashlibPassword(
     1537                    hashlib.sha384, "LOGIN", "PASSWORD"))
     1538
     1539    def test_loginLogoutWithCryptIHashMethod(self):
     1540        """
     1541        Test of the L{pb.UsernameCryptPassword} credential with a
     1542        L{cred.checkers.IChallenger} checker.
     1543        """
     1544        return self.genericTest_loginLogoutWithIHashMethod(
     1545                checker = DummyChallengerChecker(lambda u, p:p,
     1546                    LOGIN = crypt.crypt("PASSWORD", "someSalt")),
     1547                credential = pb.UsernameCryptPassword("LOGIN", "PASSWORD"))
     1548
     1549    def test_loginLogoutWithCustomIHashMethod(self):
     1550        """
     1551        Test of a custom credential with a L{cred.checkers.IChallenger}
     1552        checker.
     1553        """
     1554        return self.genericTest_loginLogoutWithIHashMethod(
     1555                checker = DummyChallengerChecker(dummyGetSalt, LOGIN = dummyHash("PASSWORD", str(len("LOGIN")))),
     1556                credential = HashMethodCredential("LOGIN", "PASSWORD"))
     1557
     1558    def test_loginWithBadHashMethod(self):
     1559        """
     1560        Test that we cannot login if client and server do not use the same
     1561        hashMethod
     1562        """
     1563        self.portal.registerChecker(
     1564            DummyChallengerChecker(dummyGetSalt, user='pass'))
     1565        factory = pb.PBClientFactory()
     1566        creds = DummyCredentialWithBadHashMethod("user", "pass")
     1567
     1568        mind = "BRAINS!"
     1569        d = factory.login(creds, mind)
     1570        self.assertFailure(d, UnauthorizedLogin, u"tete")
     1571
     1572        d = gatherResults([d])
     1573
     1574        def cleanup(ignore):
     1575            errors = self.flushLoggedErrors(UnauthorizedLogin)
     1576            self.assertEqual(len(errors), 1)
     1577            return self._disconnect(None, factory)
     1578        d.addCallback(cleanup)
     1579
     1580        connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
     1581        self.addCleanup(connector.disconnect)
     1582
     1583        return d
     1584
    14091585    def test_logoutAfterDecref(self):
    14101586        """
    14111587        If a L{RemoteReference} to an L{IPerspective} avatar is decrefed and
     
    15551731    def test_anonymousLoginWithMultipleCheckers(self):
    15561732        """
    15571733        Like L{test_anonymousLogin} but against a portal with a checker for
    1558         both IAnonymous and IUsernamePassword.
     1734        both IAnonymous and L{cred.credentials.IUsernamePassword}.
    15591735        """
    15601736        self.portal.registerChecker(checkers.AllowAnonymousAccess())
    15611737        self.portal.registerChecker(
  • cred/checkers.py

     
    261261            d.addCallback(lambda x: credentials.username)
    262262            return d
    263263
     264class IChallenger(ICredentialsChecker):
     265    """
     266    Implements me to have a checker that can be used with
     267    L{spread.pb.IHashMethod.challengeHashMethod}.
     268    """
     269    def challengeHashMethod(self, challenge, hashed_password):
     270        """
     271        Hash method used for the challenge part of authentication
    264272
     273        Returns a hash of the challenge and the hashed_password. It must be
     274        coherent with the corresponding
     275        L{spread.pb.IHashMethod.challengeHashMethod}.
     276        """
     277    def getSalt(self, username):
     278        """
     279        Return the salt used to hash the password of username.
     280        """
    265281
     282
    266283# For backwards compatibility
    267284# Allow access as the old name.
    268285OnDiskUsernamePasswordDatabase = FilePasswordDB
  • spread/pb.py

     
    2727@author: Glyph Lefkowitz
    2828"""
    2929
     30import crypt
    3031import random
    3132import types
    3233
     
    3940from twisted.cred.portal import Portal
    4041from twisted.cred.credentials import IAnonymous, ICredentials
    4142from twisted.cred.credentials import IUsernameHashedPassword, Anonymous
     43from twisted.cred.credentials import UsernamePassword, IUsernamePassword
     44from twisted.cred.checkers import IChallenger
    4245from twisted.persisted import styles
    4346from twisted.python.components import registerAdapter
    4447
     
    10411044##         obj.__del__ = reallyDel
    10421045        del self.locallyCachedObjects[objectID]
    10431046
     1047def challengeHash(challenge, hashed_password):
     1048    """
     1049    Build the response to a challenge.
    10441050
     1051    This is a part of the challenge/response authentication (see method
     1052    L{respond}).
     1053    """
     1054    m = md5()
     1055    m.update(hashed_password)
     1056    m.update(challenge)
     1057    return m.digest()
    10451058
    10461059def respond(challenge, password):
    10471060    """Respond to a challenge.
     
    10511064    m = md5()
    10521065    m.update(password)
    10531066    hashedPassword = m.digest()
    1054     m = md5()
    1055     m.update(hashedPassword)
    1056     m.update(challenge)
    1057     doubleHashedPassword = m.digest()
    1058     return doubleHashedPassword
     1067    return challengeHash(challenge, hashedPassword)
    10591068
    10601069def challenge():
    10611070    """I return some random data."""
     
    11551164        if self._broker:
    11561165            self._broker.transport.loseConnection()
    11571166
    1158     def _cbSendUsername(self, root, username, password, client):
     1167    def _cbSendUsername(self, root, username, password, client, hashMethod=None):
    11591168        return root.callRemote("login", username).addCallback(
    1160             self._cbResponse, password, client)
     1169            self._cbResponse, password, client, hashMethod)
    11611170
    1162     def _cbResponse(self, (challenge, challenger), password, client):
    1163         return challenger.callRemote("respond", respond(challenge, password), client)
     1171    def _cbResponse(self, (challenge, challenger), password, client, hashMethod=None):
     1172        if not hashMethod:
     1173            hashMethod = respond
     1174        return challenger.callRemote("respond", hashMethod(challenge, password), client)
    11641175
    11651176
    11661177    def _cbLoginAnonymous(self, root, client):
     
    12001211
    12011212        if IAnonymous.providedBy(credentials):
    12021213            d.addCallback(self._cbLoginAnonymous, client)
     1214        elif IHashMethod.providedBy(credentials):
     1215            def hashed_respond(challenge, password):
     1216                """
     1217                Respond to a challenge.
     1218
     1219                @param password: Password used to answer the challenge.
     1220                @type password: C{str}
     1221                @param challenge: One time challenge sent by the server to
     1222                    authenticate the client.
     1223                @type challenge: Tuple of two C{str}:
     1224                    - the challenge itself;
     1225                    - the salt used to hash the password.
     1226                """
     1227                return credentials.challengeHashMethod(
     1228                        challenge[0],
     1229                        credentials.pwHashMethod(password, challenge[1]))
     1230            d.addCallback(
     1231                self._cbSendUsername, credentials.username,
     1232                credentials.password, client, hashed_respond)
    12031233        else:
    12041234            d.addCallback(
    12051235                self._cbSendUsername, credentials.username,
     
    12981328            be called back with one of these values.
    12991329        """
    13001330
     1331class IHashMethod(ICredentials):
     1332    """
     1333    I encapsulate a username and a password, and the necessary method to use a
     1334    custom, possibly salted, hash, both during the challenge part of the
     1335    authentication, and to compare the password to the one of the server if
     1336    this latter is also hashed.
     1337    """
     1338    def pwHashMethod(password, salt):
     1339        """
     1340        Return the password, hashed using the salt.
     1341        """
     1342    def challengeHashMethod(challenge, data):
     1343        """
     1344        Respond to the challenge (part of authentication), using C{data} as the
     1345        hashed password.
     1346        """
    13011347
     1348class UsernameHashPassword(UsernamePassword):
     1349    """
     1350    I am a credentials used to authenticate against a hashed password.
     1351
     1352    The type of hash is implemented in my subclasses.
     1353    """
     1354    implements(IHashMethod)
     1355
     1356    def challengeHashMethod(self, challenge, data):
     1357        """
     1358        Respond to the challenge (part of authentication), using C{data} as the
     1359        hashed password.
     1360
     1361        This is a default method which can be overriden in subclasses.
     1362        """
     1363        return challengeHash(challenge, data)
     1364
     1365class UsernameHashlibPassword(UsernameHashPassword):
     1366    """
     1367    I am a credentials used to authenticate against a password hashed using the
     1368    Python hashlib library.
     1369
     1370    The type of hash is given in the constructor: C{hashmethod} is one of the
     1371    constructors of hash algorithms available in the U{Python hashlib
     1372    library<http://docs.python.org/library/hashlib.html>}.
     1373    """
     1374    def __init__(self, hashmethod, username, password):
     1375        self.hashmethod = hashmethod
     1376        self.username = username
     1377        self.password = password
     1378
     1379    def pwHashMethod(self, password, __ignored__salt):
     1380        """
     1381        Return the password, hashed using the method given in the class
     1382        constructor.
     1383
     1384        This credential does not salt the hash, so argument C{__ignored__salt},
     1385        present in the signature of the method of the parent of this class, is
     1386        ignored.
     1387        """
     1388        return self.hashmethod(password).digest()
     1389
     1390class UsernameCryptPassword(UsernameHashPassword):
     1391    """
     1392    I am a credentials used to authenticate against a password hashed using
     1393    C{crypt(3)}.
     1394    """
     1395    def pwHashMethod(self, password, salt):
     1396        """
     1397        Return the password, hashed using the C{crypt(3)} method, with the salt.
     1398        """
     1399        return crypt.crypt(password, salt)
     1400
    13021401class _PortalRoot:
    13031402    """Root object, used to login to portal."""
    13041403
     
    13601459        """
    13611460        Start of username/password login.
    13621461        """
     1462        checker = self.portal.checkers.get(IUsernamePassword, None)
    13631463        c = challenge()
    1364         return c, _PortalAuthChallenger(self.portal, self.broker, username, c)
     1464        if checker and IChallenger.providedBy(checker):
     1465            hashMethod = checker.challengeHashMethod
     1466            # As the salt may be contained in the username or hashed password
     1467            # (as for crypt(3)), we need to extract the hash here, and to send
     1468            # it to the client so that it can use it to authenticate.
     1469            client_challenge = (c, checker.getSalt(username))
     1470            server_challenge = c
     1471        else:
     1472            client_challenge = server_challenge = c
     1473            hashMethod = respond
     1474        return client_challenge, _PortalAuthChallenger(self.portal,
     1475                self.broker, username, server_challenge, hashMethod)
    13651476
    13661477
    13671478    def remote_loginAnonymous(self, mind):
     
    13851496    """
    13861497    Called with response to password challenge.
    13871498    """
    1388     implements(IUsernameHashedPassword, IUsernameMD5Password)
     1499    implements(IUsernameHashedPassword, IUsernamePassword, IUsernameMD5Password)
    13891500
    1390     def __init__(self, portal, broker, username, challenge):
     1501    def __init__(self, portal, broker, username, challenge, hashMethod):
    13911502        self.portal = portal
    13921503        self.broker = broker
    13931504        self.username = username
    13941505        self.challenge = challenge
     1506        self.hashMethod = hashMethod
    13951507
    13961508
    13971509    def remote_respond(self, response, mind):
    13981510        self.response = response
     1511        # IUsernamePassword
     1512        self.password = response
    13991513        d = self.portal.login(self, mind, IPerspective)
    14001514        d.addCallback(self._cbLogin)
    14011515        return d
     
    14031517
    14041518    # IUsernameHashedPassword:
    14051519    def checkPassword(self, password):
    1406         return self.checkMD5Password(md5(password).digest())
     1520        return self.checkHashedPassword(self.hashMethod(self.challenge, password))
    14071521
    1408 
    14091522    # IUsernameMD5Password
    14101523    def checkMD5Password(self, md5Password):
    14111524        md = md5()
     
    14141527        correct = md.digest()
    14151528        return self.response == correct
    14161529
     1530    def checkHashedPassword(self, hashed):
     1531        return hashed == self.response
    14171532
    14181533__all__ = [
    14191534    # Everything from flavors is exposed publically here.
     
    14301545    'RemoteMethod', 'IPerspective', 'Avatar', 'AsReferenceable',
    14311546    'RemoteReference', 'CopyableFailure', 'CopiedFailure', 'failure2Copyable',
    14321547    'Broker', 'respond', 'challenge', 'PBClientFactory', 'PBServerFactory',
    1433     'IUsernameMD5Password',
     1548    'IUsernameMD5Password', 'IHashMethod',
    14341549    ]