~ubuntu-branches/ubuntu/hardy/pymsn/hardy-proposed

« back to all changes in this revision

Viewing changes to pymsn/service/SingleSignOn.py

  • Committer: Bazaar Package Importer
  • Author(s): Laurent Bigonville, Sjoerd Simons, Laurent Bigonville, Jonny Lamb
  • Date: 2008-01-17 18:23:14 UTC
  • mfrom: (1.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20080117182314-lwymmpnk2ut3rvr1
Tags: 0.3.1-0ubuntu1
[ Sjoerd Simons ]
* debian/rules: remove dh_python, it's no longer needed

[ Laurent Bigonville ]
* New upstream release (0.3.1)
* debian/control:
  - Add myself as an Uploaders
  - Add python:Provides for binary package
  - Add python-ctypes and python-crypto to build-deps/deps
* debian/rules: remove binary-install rule
* Add watch file
* remove pycompat file, not needed anymore
* Modify Maintainer value to match the DebianMaintainerField
  specification.

[ Jonny Lamb ]
* Added python-adns to build-deps/deps.
* Added python-pyopenssl to build-deps/deps.
* Updated copyright.
* Upped Standards-Version to 3.7.3.
* Added "XS-Dm-Upload-Allowed: yes" under the request of Sjoerd Simons.
* Added myself to Uploaders.
* Added Homepage to control.
* Added Vcs-Bzr to control.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# pymsn - a python client library for Msn
 
4
#
 
5
# Copyright (C) 2005-2007 Ali Sabil <ali.sabil@gmail.com>
 
6
#
 
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.
 
11
#
 
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.
 
16
#
 
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
 
20
 
 
21
from SOAPService import *
 
22
from description.SingleSignOn.RequestMultipleSecurityTokens import LiveService
 
23
import pymsn.storage
 
24
 
 
25
import base64
 
26
import struct
 
27
import time
 
28
import datetime
 
29
import sys
 
30
import Crypto.Util.randpool as randpool
 
31
from Crypto.Hash import HMAC, SHA
 
32
from Crypto.Cipher import DES3
 
33
 
 
34
__all__ = ['SingleSignOn', 'LiveService', 'RequireSecurityTokens']
 
35
 
 
36
class SecurityToken(object):
 
37
    
 
38
    def __init__(self):
 
39
        self.type = ""
 
40
        self.service_address = ""
 
41
        self.lifetime = [0, 0]
 
42
        self.security_token = ""
 
43
        self.proof_token = ""
 
44
 
 
45
    def is_expired(self):
 
46
        return datetime.datetime.utcnow() + datetime.timedelta(seconds=60) \
 
47
                >= self.lifetime[1]
 
48
 
 
49
    def mbi_crypt(self, nonce):
 
50
        WINCRYPT_CRYPT_MODE_CBC = 1
 
51
        WINCRYPT_CALC_3DES      = 0x6603
 
52
        WINCRYPT_CALC_SHA1      = 0x8004
 
53
 
 
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")
 
58
 
 
59
        # Create a HMAC-SHA-1 hash of nonce using key2
 
60
        hash = HMAC.new(key2, nonce, SHA).digest()
 
61
 
 
62
        #
 
63
        # Encrypt nonce with DES3 using key3
 
64
        #
 
65
 
 
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)
 
69
 
 
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")
 
73
 
 
74
        blob = struct.pack("<LLLLLLL", 28, WINCRYPT_CRYPT_MODE_CBC,
 
75
                WINCRYPT_CALC_3DES, WINCRYPT_CALC_SHA1, len(iv), len(hash),
 
76
                len(ciph))
 
77
        blob += iv + hash + ciph
 
78
        return base64.b64encode(blob)
 
79
 
 
80
    def _derive_key(self, key, magic):
 
81
        hash1 = HMAC.new(key, magic, SHA).digest()
 
82
        hash2 = HMAC.new(key, hash1 + magic, SHA).digest()
 
83
 
 
84
        hash3 = HMAC.new(key, hash1, SHA).digest()            
 
85
        hash4 = HMAC.new(key, hash3 + magic, SHA).digest()
 
86
        return hash2 + hash4[0:4]
 
87
 
 
88
    def __str__(self):
 
89
        return self.security_token
 
90
 
 
91
    def __repr__(self):
 
92
        return "<SecurityToken type=\"%s\" address=\"%s\" lifetime=\"%s\">" % \
 
93
                (self.type, self.service_address, str(self.lifetime))
 
94
 
 
95
 
 
96
class RequireSecurityTokens(object):
 
97
    def __init__(self, *tokens):
 
98
        assert(len(tokens) > 0)
 
99
        self._tokens = tokens
 
100
 
 
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)
 
106
 
 
107
        def method(object, callback, errback, *args, **kwargs):
 
108
            callback = (sso_callback, object, callback, errback, args, kwargs)
 
109
            object._sso.RequestMultipleSecurityTokens(callback,
 
110
                    None, *self._tokens)
 
111
        method.__name__ = func.__name__
 
112
        method.__doc__ = func.__doc__
 
113
        method.__dict__.update(func.__dict__)
 
114
        return method
 
115
 
 
116
 
 
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,
 
121
                "security-tokens")
 
122
 
 
123
        self.__pending_response = False
 
124
        self.__pending_requests = []
 
125
        SOAPService.__init__(self, "SingleSignOn", proxies)
 
126
        self.url = self._service.url
 
127
 
 
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}"""
 
133
 
 
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))
 
139
            return
 
140
 
 
141
        method = self._service.RequestMultipleSecurityTokens
 
142
 
 
143
        response_tokens = {}
 
144
        
 
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:
 
150
                try:
 
151
                    token = self.__storage[service_url]
 
152
                except pymsn.storage.DecryptError:
 
153
                    continue
 
154
                if not token.is_expired():
 
155
                    services.remove(service)
 
156
                    response_tokens[service] = token
 
157
 
 
158
        if len(services) == 0:
 
159
            self._HandleRequestMultipleSecurityTokensResponse(callback,
 
160
                    errback, [], (requested_services, response_tokens))
 
161
            return
 
162
 
 
163
        http_headers = method.transport_headers()
 
164
        soap_action = method.soap_action()
 
165
        
 
166
        soap_header = method.soap_header(*self.__credentials)
 
167
        soap_body = method.soap_body(*services)
 
168
 
 
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))
 
173
    
 
174
    def _HandleRequestMultipleSecurityTokensResponse(self, callback, errback,
 
175
            response, user_data):
 
176
        requested_services, response_tokens = user_data
 
177
        result = {}
 
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")
 
185
            
 
186
            try:
 
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")
 
193
 
 
194
            try:
 
195
                token.proof_token = node.findtext("./wst:RequestedProofToken/wst:BinarySecret")
 
196
            except AttributeError:
 
197
                pass
 
198
 
 
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)
 
205
 
 
206
        self.__pending_response = False
 
207
 
 
208
        if callback is not None:
 
209
            callback[0](result, *callback[1:])
 
210
 
 
211
        if len(self.__pending_requests):
 
212
            callback, errback, services = self.__pending_requests.pop(0)
 
213
            self.RequestMultipleSecurityTokens(callback, errback, *services)
 
214
 
 
215
    def DiscardSecurityTokens(self, services):
 
216
        for service in services:
 
217
            del self.__storage[service[0]]
 
218
 
 
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)
 
228
            
 
229
            
 
230
            
 
231
        
 
232
 
 
233
if __name__ == '__main__':
 
234
    import sys
 
235
    import getpass
 
236
    import signal
 
237
    import gobject
 
238
    import logging
 
239
 
 
240
    def sso_cb(tokens):
 
241
        print "Received tokens : "
 
242
        for token in tokens:
 
243
            print "token %s : %s" % (token, str(tokens[token]))
 
244
 
 
245
    logging.basicConfig(level=logging.DEBUG)
 
246
 
 
247
    if len(sys.argv) < 2:
 
248
        account = raw_input('Account: ')
 
249
    else:
 
250
        account = sys.argv[1]
 
251
 
 
252
    if len(sys.argv) < 3:
 
253
        password = getpass.getpass('Password: ')
 
254
    else:
 
255
        password = sys.argv[2]
 
256
 
 
257
    mainloop = gobject.MainLoop(is_running=True)
 
258
    
 
259
    signal.signal(signal.SIGTERM,
 
260
            lambda *args: gobject.idle_add(mainloop.quit()))
 
261
 
 
262
    sso = SingleSignOn(account, password)
 
263
    sso.RequestMultipleSecurityTokens((sso_cb,), None, LiveService.VOICE)
 
264
 
 
265
    while mainloop.is_running():
 
266
        try:
 
267
            mainloop.run()
 
268
        except KeyboardInterrupt:
 
269
            mainloop.quit()
 
270