1
# -*- test-case-name: epsilon.test.test_ampauth -*-
2
# Copyright (c) 2008 Divmod. See LICENSE for details.
5
This module provides integration between L{AMP<twisted.protocols.amp.AMP>} and
9
from hashlib import sha1
11
from zope.interface import implements
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
20
from epsilon.iepsilon import IOneTimePad
21
from epsilon.structlike import record
26
class UnhandledCredentials(Exception):
28
L{login} was passed a credentials object which did not provide a recognized
29
credentials interface.
34
class OTPLogin(Command):
36
Command to initiate a login attempt where a one-time pad is to be used in
37
place of username/password credentials.
39
arguments = [('pad', String())]
42
# Invalid username or password
43
UnauthorizedLogin: 'UNAUTHORIZED_LOGIN',
44
# No IBoxReceiver avatar
45
NotImplementedError: 'NOT_IMPLEMENTED_ERROR'}
49
class PasswordLogin(Command):
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.
55
arguments = [('username', String())]
56
response = [('challenge', String())]
60
def _calcResponse(challenge, nonce, password):
62
Compute the response to the given challenge.
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.
69
@param nonce: An arbitrary byte string, generated by the client to include
70
in the hash to avoid making the client an oracle.
72
@type password: C{str}
73
@param password: The known correct password for the account being
77
@return: A hash constructed from the three parameters.
79
return sha1('%s %s %s' % (challenge, nonce, password)).digest()
83
class PasswordChallengeResponse(Command):
85
Command to respond to a challenge issued in the response to a
86
L{PasswordLogin} command and complete a username/password-based login
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.
92
arguments = [('cnonce', String()),
93
('response', String())]
96
# Invalid username or password
97
UnauthorizedLogin: 'UNAUTHORIZED_LOGIN',
98
# No IBoxReceiver avatar
99
NotImplementedError: 'NOT_IMPLEMENTED_ERROR'}
102
def determineFrom(cls, challenge, password):
104
Create a nonce and use it, along with the given challenge and password,
105
to generate the parameters for a response.
107
@return: A C{dict} suitable to be used as the keyword arguments when
108
calling this command.
110
nonce = secureRandom(16)
111
response = _calcResponse(challenge, nonce, password)
112
return dict(cnonce=nonce, response=response)
116
class _AMPUsernamePassword(record('username challenge nonce response')):
118
L{IUsernameHashedPassword} implementation used by L{PasswordLogin} and
121
implements(IUsernameHashedPassword)
123
def checkPassword(self, password):
125
Check the given plaintext password against the response in this
128
@type password: C{str}
129
@param password: The known correct password associated with
132
@return: A C{bool}, C{True} if this credentials object agrees with the
133
given password, C{False} otherwise.
135
if isinstance(password, unicode):
136
password = password.encode('utf-8')
137
correctResponse = _calcResponse(self.challenge, self.nonce, password)
138
return correctResponse == self.response
142
class _AMPOneTimePad(record('padValue')):
144
L{IOneTimePad} implementation used by L{OTPLogin}.
146
@ivar padValue: The value of the one-time pad.
147
@type padValue: C{str}
149
implements(IOneTimePad)
153
class CredReceiver(AMP):
155
Integration between AMP and L{twisted.cred}.
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.
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
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
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.
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.
181
@PasswordLogin.responder
182
def passwordLogin(self, username):
184
Generate a new challenge for the given username.
186
self.challenge = secureRandom(16)
187
self.username = username
188
return {'challenge': self.challenge}
191
def _login(self, credentials):
193
Actually login to our portal with the given credentials.
195
d = self.portal.login(credentials, None, IBoxReceiver)
196
def cbLoggedIn((interface, avatar, logout)):
198
self.boxReceiver = avatar
199
self.boxReceiver.startReceivingBoxes(self.boxSender)
201
d.addCallback(cbLoggedIn)
205
@PasswordChallengeResponse.responder
206
def passwordChallengeResponse(self, cnonce, response):
208
Verify the response to a challenge.
210
return self._login(_AMPUsernamePassword(
211
self.username, self.challenge, cnonce, response))
215
def otpLogin(self, pad):
217
Verify the given pad.
219
return self._login(_AMPOneTimePad(pad))
222
def connectionLost(self, reason):
224
If a login has happened, perform a logout.
226
AMP.connectionLost(self, reason)
227
if self.logout is not None:
229
self.boxReceiver = self.logout = None
233
class OneTimePadChecker(record('pads')):
235
Checker which validates one-time pads.
237
@ivar pads: Mapping between valid one-time pads and avatar IDs.
240
implements(ICredentialsChecker)
242
credentialInterfaces = (IOneTimePad,)
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')
252
class CredAMPServerFactory(ServerFactory):
254
Server factory useful for creating L{CredReceiver} instances.
256
This factory takes care of associating a L{Portal} with L{CredReceiver}
257
instances it creates.
259
@ivar portal: The portal which will be used by L{CredReceiver} instances
260
created by this factory.
262
protocol = CredReceiver
264
def __init__(self, portal):
268
def buildProtocol(self, addr):
269
proto = ServerFactory.buildProtocol(self, addr)
270
proto.portal = self.portal
275
def login(client, credentials):
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}.
281
@param client: A connected L{AMP} instance which will be used to issue
282
authentication commands.
284
@param credentials: An object providing L{IUsernamePassword} which will
285
be used to authenticate this connection to the server.
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.
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)
306
'UnhandledCredentials',
308
'OTPLogin', 'OneTimePadChecker',
310
'PasswordLogin', 'PasswordChallengeResponse', 'CredReceiver',
312
'CredAMPServerFactory', 'login']