1
# -*- coding: utf-8 -*-
3
# pymsn - a python client library for Msn
5
# Copyright (C) 2005-2007 Ali Sabil <ali.sabil@gmail.com>
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 2 of the License, or
10
# (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program; if not, write to the Free Software
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from SOAPService import *
22
from description.SingleSignOn.RequestMultipleSecurityTokens import LiveService
30
import Crypto.Util.randpool as randpool
31
from Crypto.Hash import HMAC, SHA
32
from Crypto.Cipher import DES3
34
__all__ = ['SingleSignOn', 'LiveService', 'RequireSecurityTokens']
36
class SecurityToken(object):
40
self.service_address = ""
41
self.lifetime = [0, 0]
42
self.security_token = ""
46
return datetime.datetime.utcnow() + datetime.timedelta(seconds=60) \
49
def mbi_crypt(self, nonce):
50
WINCRYPT_CRYPT_MODE_CBC = 1
51
WINCRYPT_CALC_3DES = 0x6603
52
WINCRYPT_CALC_SHA1 = 0x8004
54
# Read key and generate two derived keys
55
key1 = base64.b64decode(self.proof_token)
56
key2 = self._derive_key(key1, "WS-SecureConversationSESSION KEY HASH")
57
key3 = self._derive_key(key1, "WS-SecureConversationSESSION KEY ENCRYPTION")
59
# Create a HMAC-SHA-1 hash of nonce using key2
60
hash = HMAC.new(key2, nonce, SHA).digest()
63
# Encrypt nonce with DES3 using key3
66
# IV (Initialization Vector): 8 bytes of random data
67
iv = randpool.RandomPool().get_bytes(8)
68
obj = DES3.new(key3, DES3.MODE_CBC, iv)
70
# XXX: win32's Crypt API seems to pad the input with 0x08 bytes
71
# to align on 72/36/18/9 boundary
72
ciph = obj.encrypt(nonce + "\x08\x08\x08\x08\x08\x08\x08\x08")
74
blob = struct.pack("<LLLLLLL", 28, WINCRYPT_CRYPT_MODE_CBC,
75
WINCRYPT_CALC_3DES, WINCRYPT_CALC_SHA1, len(iv), len(hash),
77
blob += iv + hash + ciph
78
return base64.b64encode(blob)
80
def _derive_key(self, key, magic):
81
hash1 = HMAC.new(key, magic, SHA).digest()
82
hash2 = HMAC.new(key, hash1 + magic, SHA).digest()
84
hash3 = HMAC.new(key, hash1, SHA).digest()
85
hash4 = HMAC.new(key, hash3 + magic, SHA).digest()
86
return hash2 + hash4[0:4]
89
return self.security_token
92
return "<SecurityToken type=\"%s\" address=\"%s\" lifetime=\"%s\">" % \
93
(self.type, self.service_address, str(self.lifetime))
96
class RequireSecurityTokens(object):
97
def __init__(self, *tokens):
98
assert(len(tokens) > 0)
101
def __call__(self, func):
102
def sso_callback(tokens, object, user_callback, user_errback,
103
user_args, user_kwargs):
104
object._tokens.update(tokens)
105
func(object, user_callback, user_errback, *user_args, **user_kwargs)
107
def method(object, callback, errback, *args, **kwargs):
108
callback = (sso_callback, object, callback, errback, args, kwargs)
109
object._sso.RequestMultipleSecurityTokens(callback,
111
method.__name__ = func.__name__
112
method.__doc__ = func.__doc__
113
method.__dict__.update(func.__dict__)
117
class SingleSignOn(SOAPService):
118
def __init__(self, username, password, proxies=None):
119
self.__credentials = (username, password)
120
self.__storage = pymsn.storage.get_storage(username, password,
123
self.__pending_response = False
124
self.__pending_requests = []
125
SOAPService.__init__(self, "SingleSignOn", proxies)
126
self.url = self._service.url
128
def RequestMultipleSecurityTokens(self, callback, errback, *services):
129
"""Requests multiple security tokens from the single sign on service.
130
@param callback: tuple(callable, *args)
131
@param errback: tuple(callable, *args)
132
@param services: one or more L{LiveService}"""
134
#FIXME: we should instead check what are the common requested tokens
135
# and if some tokens are not common then do a parallel request, needs
136
# to be fixed later, for now, we just queue the requests
137
if self.__pending_response:
138
self.__pending_requests.append((callback, errback, services))
141
method = self._service.RequestMultipleSecurityTokens
145
requested_services = services
146
services = list(services)
147
for service in services: # filter already available tokens
148
service_url = service[0]
149
if service_url in self.__storage:
151
token = self.__storage[service_url]
152
except pymsn.storage.DecryptError:
154
if not token.is_expired():
155
services.remove(service)
156
response_tokens[service] = token
158
if len(services) == 0:
159
self._HandleRequestMultipleSecurityTokensResponse(callback,
160
errback, [], (requested_services, response_tokens))
163
http_headers = method.transport_headers()
164
soap_action = method.soap_action()
166
soap_header = method.soap_header(*self.__credentials)
167
soap_body = method.soap_body(*services)
169
self.__pending_response = True
170
self._send_request("RequestMultipleSecurityTokens", self.url,
171
soap_header, soap_body, soap_action,
172
callback, errback, http_headers, (requested_services, response_tokens))
174
def _HandleRequestMultipleSecurityTokensResponse(self, callback, errback,
175
response, user_data):
176
requested_services, response_tokens = user_data
178
for node in response:
179
token = SecurityToken()
180
token.type = node.findtext("./wst:TokenType")
181
token.service_address = node.findtext("./wsp:AppliesTo"
182
"/wsa:EndpointReference/wsa:Address")
183
token.lifetime[0] = node.findtext("./wst:LifeTime/wsu:Created", "datetime")
184
token.lifetime[1] = node.findtext("./wst:LifeTime/wsu:Expires", "datetime")
187
token.security_token = node.findtext("./wst:RequestedSecurityToken"
188
"/wsse:BinarySecurityToken")
189
except AttributeError:
190
token.security_token = node.findtext("./wst:RequestedSecurityToken"
191
"/xmlenc:EncryptedData/xmlenc:CipherData"
192
"/xmlenc:CipherValue")
195
token.proof_token = node.findtext("./wst:RequestedProofToken/wst:BinarySecret")
196
except AttributeError:
199
service = LiveService.url_to_service(token.service_address)
200
assert(service != None), "Unknown service URL : " + \
201
token.service_address
202
self.__storage[token.service_address] = token
203
result[service] = token
204
result.update(response_tokens)
206
self.__pending_response = False
208
if callback is not None:
209
callback[0](result, *callback[1:])
211
if len(self.__pending_requests):
212
callback, errback, services = self.__pending_requests.pop(0)
213
self.RequestMultipleSecurityTokens(callback, errback, *services)
215
def DiscardSecurityTokens(self, services):
216
for service in services:
217
del self.__storage[service[0]]
219
def _HandleSOAPFault(self, request_id, callback, errback,
220
soap_response, user_data):
221
if soap_response.fault.faultcode.endswith("FailedAuthentication"):
222
errback[0](*errback[1:])
223
elif soap_response.fault.faultcode.endswith("Redirect"):
224
requested_services, response_tokens = user_data
225
self.url = soap_response.fault.tree.findtext("psf:redirectUrl")
226
self.__pending_response = False
227
self.RequestMultipleSecurityTokens(callback, errback, *requested_services)
233
if __name__ == '__main__':
241
print "Received tokens : "
243
print "token %s : %s" % (token, str(tokens[token]))
245
logging.basicConfig(level=logging.DEBUG)
247
if len(sys.argv) < 2:
248
account = raw_input('Account: ')
250
account = sys.argv[1]
252
if len(sys.argv) < 3:
253
password = getpass.getpass('Password: ')
255
password = sys.argv[2]
257
mainloop = gobject.MainLoop(is_running=True)
259
signal.signal(signal.SIGTERM,
260
lambda *args: gobject.idle_add(mainloop.quit()))
262
sso = SingleSignOn(account, password)
263
sso.RequestMultipleSecurityTokens((sso_cb,), None, LiveService.VOICE)
265
while mainloop.is_running():
268
except KeyboardInterrupt: