~divmod-dev/divmod.org/trunk

« back to all changes in this revision

Viewing changes to Epsilon/epsilon/ampauth.py

  • Committer: Jean-Paul Calderone
  • Date: 2014-06-29 20:33:04 UTC
  • mfrom: (2749.1.1 remove-epsilon-1325289)
  • Revision ID: exarkun@twistedmatrix.com-20140629203304-gdkmbwl1suei4m97
mergeĀ lp:~exarkun/divmod.org/remove-epsilon-1325289

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: epsilon.test.test_ampauth -*-
2
 
# Copyright (c) 2008 Divmod.  See LICENSE for details.
3
 
 
4
 
"""
5
 
This module provides integration between L{AMP<twisted.protocols.amp.AMP>} and
6
 
L{cred<twisted.cred>}.
7
 
"""
8
 
 
9
 
from hashlib import sha1
10
 
 
11
 
from zope.interface import implements
12
 
 
13
 
from twisted.python.randbytes import secureRandom
14
 
from twisted.cred.error import UnauthorizedLogin
15
 
from twisted.cred.credentials import IUsernameHashedPassword, IUsernamePassword
16
 
from twisted.cred.checkers import ICredentialsChecker
17
 
from twisted.protocols.amp import IBoxReceiver, String, Command, AMP
18
 
from twisted.internet.protocol import ServerFactory
19
 
 
20
 
from epsilon.iepsilon import IOneTimePad
21
 
from epsilon.structlike import record
22
 
 
23
 
__metaclass__ = type
24
 
 
25
 
 
26
 
class UnhandledCredentials(Exception):
27
 
    """
28
 
    L{login} was passed a credentials object which did not provide a recognized
29
 
    credentials interface.
30
 
    """
31
 
 
32
 
 
33
 
 
34
 
class OTPLogin(Command):
35
 
    """
36
 
    Command to initiate a login attempt where a one-time pad is to be used in
37
 
    place of username/password credentials.
38
 
    """
39
 
    arguments = [('pad', String())]
40
 
 
41
 
    errors = {
42
 
        # Invalid username or password
43
 
        UnauthorizedLogin: 'UNAUTHORIZED_LOGIN',
44
 
        # No IBoxReceiver avatar
45
 
        NotImplementedError: 'NOT_IMPLEMENTED_ERROR'}
46
 
 
47
 
 
48
 
 
49
 
class PasswordLogin(Command):
50
 
    """
51
 
    Command to initiate a username/password-based login attempt.  The response
52
 
    to this command is a challenge which must be responded to based on the
53
 
    correct password associated with the username given to this command.
54
 
    """
55
 
    arguments = [('username', String())]
56
 
    response = [('challenge', String())]
57
 
 
58
 
 
59
 
 
60
 
def _calcResponse(challenge, nonce, password):
61
 
    """
62
 
    Compute the response to the given challenge.
63
 
 
64
 
    @type challenge: C{str}
65
 
    @param challenge: An arbitrary byte string, probably received in response
66
 
        to (or generated for) the L{PasswordLogin} command.
67
 
 
68
 
    @type nonce: C{str}
69
 
    @param nonce: An arbitrary byte string, generated by the client to include
70
 
        in the hash to avoid making the client an oracle.
71
 
 
72
 
    @type password: C{str}
73
 
    @param password: The known correct password for the account being
74
 
        authenticated.
75
 
 
76
 
    @rtype: C{str}
77
 
    @return: A hash constructed from the three parameters.
78
 
    """
79
 
    return sha1('%s %s %s' % (challenge, nonce, password)).digest()
80
 
 
81
 
 
82
 
 
83
 
class PasswordChallengeResponse(Command):
84
 
    """
85
 
    Command to respond to a challenge issued in the response to a
86
 
    L{PasswordLogin} command and complete a username/password-based login
87
 
    attempt.
88
 
 
89
 
    @param cnonce: A randomly generated string used only in this response.
90
 
    @param response: The SHA-1 hash of the challenge, cnonce, and password.
91
 
    """
92
 
    arguments = [('cnonce', String()),
93
 
                 ('response', String())]
94
 
 
95
 
    errors = {
96
 
        # Invalid username or password
97
 
        UnauthorizedLogin: 'UNAUTHORIZED_LOGIN',
98
 
        # No IBoxReceiver avatar
99
 
        NotImplementedError: 'NOT_IMPLEMENTED_ERROR'}
100
 
 
101
 
    @classmethod
102
 
    def determineFrom(cls, challenge, password):
103
 
        """
104
 
        Create a nonce and use it, along with the given challenge and password,
105
 
        to generate the parameters for a response.
106
 
 
107
 
        @return: A C{dict} suitable to be used as the keyword arguments when
108
 
            calling this command.
109
 
        """
110
 
        nonce = secureRandom(16)
111
 
        response = _calcResponse(challenge, nonce, password)
112
 
        return dict(cnonce=nonce, response=response)
113
 
 
114
 
 
115
 
 
116
 
class _AMPUsernamePassword(record('username challenge nonce response')):
117
 
    """
118
 
    L{IUsernameHashedPassword} implementation used by L{PasswordLogin} and
119
 
    related commands.
120
 
    """
121
 
    implements(IUsernameHashedPassword)
122
 
 
123
 
    def checkPassword(self, password):
124
 
        """
125
 
        Check the given plaintext password against the response in this
126
 
        credentials object.
127
 
 
128
 
        @type password: C{str}
129
 
        @param password: The known correct password associated with
130
 
            C{self.username}.
131
 
 
132
 
        @return: A C{bool}, C{True} if this credentials object agrees with the
133
 
            given password, C{False} otherwise.
134
 
        """
135
 
        if isinstance(password, unicode):
136
 
            password = password.encode('utf-8')
137
 
        correctResponse = _calcResponse(self.challenge, self.nonce, password)
138
 
        return correctResponse == self.response
139
 
 
140
 
 
141
 
 
142
 
class _AMPOneTimePad(record('padValue')):
143
 
    """
144
 
    L{IOneTimePad} implementation used by L{OTPLogin}.
145
 
 
146
 
    @ivar padValue: The value of the one-time pad.
147
 
    @type padValue: C{str}
148
 
    """
149
 
    implements(IOneTimePad)
150
 
 
151
 
 
152
 
 
153
 
class CredReceiver(AMP):
154
 
    """
155
 
    Integration between AMP and L{twisted.cred}.
156
 
 
157
 
    This implementation is limited to a single authentication per connection.
158
 
    A future implementation may use I{routes} to allow multiple authentications
159
 
    over the same connection.
160
 
 
161
 
    @ivar portal: The L{Portal} against which login will be performed.  This is
162
 
        expected to be set by the factory which creates instances of this
163
 
        class.
164
 
 
165
 
    @ivar logout: C{None} or a no-argument callable.  This is set to the logout
166
 
        object returned by L{Portal.login} and is set while an avatar is logged
167
 
        in.
168
 
 
169
 
    @ivar challenge: The C{str} which was sent as a challenge in response to
170
 
        the L{PasswordLogin} command.  If multiple L{PasswordLogin} commands
171
 
        are sent, this is the challenge sent in response to the most recent of
172
 
        them.  It is not set before L{PasswordLogin} is received.
173
 
 
174
 
    @ivar username: The C{str} which was received for the I{username} parameter
175
 
        of the L{PasswordLogin} command.  The lifetime is the same as that of
176
 
        the I{challenge} attribute.
177
 
    """
178
 
    portal = None
179
 
    logout = None
180
 
 
181
 
    @PasswordLogin.responder
182
 
    def passwordLogin(self, username):
183
 
        """
184
 
        Generate a new challenge for the given username.
185
 
        """
186
 
        self.challenge = secureRandom(16)
187
 
        self.username = username
188
 
        return {'challenge': self.challenge}
189
 
 
190
 
 
191
 
    def _login(self, credentials):
192
 
        """
193
 
        Actually login to our portal with the given credentials.
194
 
        """
195
 
        d = self.portal.login(credentials, None, IBoxReceiver)
196
 
        def cbLoggedIn((interface, avatar, logout)):
197
 
            self.logout = logout
198
 
            self.boxReceiver = avatar
199
 
            self.boxReceiver.startReceivingBoxes(self.boxSender)
200
 
            return {}
201
 
        d.addCallback(cbLoggedIn)
202
 
        return d
203
 
 
204
 
 
205
 
    @PasswordChallengeResponse.responder
206
 
    def passwordChallengeResponse(self, cnonce, response):
207
 
        """
208
 
        Verify the response to a challenge.
209
 
        """
210
 
        return self._login(_AMPUsernamePassword(
211
 
            self.username, self.challenge, cnonce, response))
212
 
 
213
 
 
214
 
    @OTPLogin.responder
215
 
    def otpLogin(self, pad):
216
 
        """
217
 
        Verify the given pad.
218
 
        """
219
 
        return self._login(_AMPOneTimePad(pad))
220
 
 
221
 
 
222
 
    def connectionLost(self, reason):
223
 
        """
224
 
        If a login has happened, perform a logout.
225
 
        """
226
 
        AMP.connectionLost(self, reason)
227
 
        if self.logout is not None:
228
 
            self.logout()
229
 
            self.boxReceiver = self.logout = None
230
 
 
231
 
 
232
 
 
233
 
class OneTimePadChecker(record('pads')):
234
 
    """
235
 
    Checker which validates one-time pads.
236
 
 
237
 
    @ivar pads: Mapping between valid one-time pads and avatar IDs.
238
 
    @type pads: C{dict}
239
 
    """
240
 
    implements(ICredentialsChecker)
241
 
 
242
 
    credentialInterfaces = (IOneTimePad,)
243
 
 
244
 
    # ICredentialsChecker
245
 
    def requestAvatarId(self, credentials):
246
 
        if credentials.padValue in self.pads:
247
 
            return self.pads.pop(credentials.padValue)
248
 
        raise UnauthorizedLogin('Unknown one-time pad')
249
 
 
250
 
 
251
 
 
252
 
class CredAMPServerFactory(ServerFactory):
253
 
    """
254
 
    Server factory useful for creating L{CredReceiver} instances.
255
 
 
256
 
    This factory takes care of associating a L{Portal} with L{CredReceiver}
257
 
    instances it creates.
258
 
 
259
 
    @ivar portal: The portal which will be used by L{CredReceiver} instances
260
 
        created by this factory.
261
 
    """
262
 
    protocol = CredReceiver
263
 
 
264
 
    def __init__(self, portal):
265
 
        self.portal = portal
266
 
 
267
 
 
268
 
    def buildProtocol(self, addr):
269
 
        proto = ServerFactory.buildProtocol(self, addr)
270
 
        proto.portal = self.portal
271
 
        return proto
272
 
 
273
 
 
274
 
 
275
 
def login(client, credentials):
276
 
    """
277
 
    Authenticate using the given L{AMP} instance.  The protocol must be
278
 
    connected to a server with responders for L{PasswordLogin} and
279
 
    L{PasswordChallengeResponse}.
280
 
 
281
 
    @param client: A connected L{AMP} instance which will be used to issue
282
 
        authentication commands.
283
 
 
284
 
    @param credentials: An object providing L{IUsernamePassword} which will
285
 
        be used to authenticate this connection to the server.
286
 
 
287
 
    @return: A L{Deferred} which fires when authentication has succeeded or
288
 
        which fails with L{UnauthorizedLogin} if the server rejects the
289
 
        authentication attempt.
290
 
    """
291
 
    if not IUsernamePassword.providedBy(credentials):
292
 
        raise UnhandledCredentials()
293
 
    d = client.callRemote(
294
 
        PasswordLogin, username=credentials.username)
295
 
    def cbChallenge(response):
296
 
        args = PasswordChallengeResponse.determineFrom(
297
 
            response['challenge'], credentials.password)
298
 
        d = client.callRemote(PasswordChallengeResponse, **args)
299
 
        return d.addCallback(lambda ignored: client)
300
 
    d.addCallback(cbChallenge)
301
 
    return d
302
 
 
303
 
 
304
 
 
305
 
__all__ = [
306
 
    'UnhandledCredentials',
307
 
 
308
 
    'OTPLogin', 'OneTimePadChecker',
309
 
 
310
 
    'PasswordLogin', 'PasswordChallengeResponse', 'CredReceiver',
311
 
 
312
 
    'CredAMPServerFactory', 'login']