11# coding: utf-8
22import hashlib
3+ import random
4+ import string
35from base64 import b64decode
46from binascii import hexlify
57import requests
1315 LastPassInvalidPasswordError ,
1416 LastPassIncorrectGoogleAuthenticatorCodeError ,
1517 LastPassIncorrectYubikeyPasswordError ,
18+ LastPassIncorrectOutOfBandRequiredError ,
19+ LastPassIncorrectMultiFactorResponseError ,
1620 LastPassUnknownError
1721)
1822from .session import Session
2125http = requests
2226
2327
24- def login (username , password , multifactor_password = None , client_id = None ):
28+ def login (username , password , multifactor_password = None , client_id = None , trust_id = None , trust_me = False ):
2529 key_iteration_count = request_iteration_count (username )
26- return request_login (username , password , key_iteration_count , multifactor_password , client_id )
30+ return request_login (username , password , key_iteration_count , multifactor_password , client_id , trust_id = trust_id , trust_me = trust_me )
2731
2832
2933def logout (session , web_client = http ):
@@ -63,21 +67,31 @@ def request_iteration_count(username, web_client=http):
6367 raise InvalidResponseError ('Key iteration count is not positive' )
6468
6569
66- def request_login (username , password , key_iteration_count , multifactor_password = None , client_id = None , web_client = http ):
70+ def request_login (username , password , key_iteration_count , multifactor_password = None , client_id = None , web_client = http , trust_id = None , trust_me = False ):
6771 body = {
68- 'method' : 'mobile' ,
69- 'web' : 1 ,
70- 'xml' : 1 ,
72+ 'method' : 'cli' ,
73+ 'xml' : 2 ,
7174 'username' : username ,
7275 'hash' : make_hash (username , password , key_iteration_count ),
7376 'iterations' : key_iteration_count ,
77+ 'includeprivatekeyenc' : 1 ,
78+ 'outofbandsupported' : 1
7479 }
7580
7681 if multifactor_password :
7782 body ['otp' ] = multifactor_password
7883
84+ # if client_id:
85+ # body['imei'] = client_id
86+
87+ if trust_me and not trust_id :
88+ trust_id = generate_trust_id ()
89+
90+ if trust_id :
91+ body ['uuid' ] = trust_id
92+
7993 if client_id :
80- body ['imei ' ] = client_id
94+ body ['trustlabel ' ] = client_id
8195
8296 response = web_client .post ('https://lastpass.com/login.php' ,
8397 data = body )
@@ -93,17 +107,83 @@ def request_login(username, password, key_iteration_count, multifactor_password=
93107 if parsed_response is None :
94108 raise InvalidResponseError ()
95109
96- session = create_session (parsed_response , key_iteration_count )
110+ session = create_session (parsed_response , key_iteration_count , trust_id )
97111 if not session :
98- raise login_error (parsed_response )
112+ try :
113+ raise login_error (parsed_response )
114+ except LastPassIncorrectOutOfBandRequiredError :
115+ (session , parsed_response ) = oob_login (web_client , parsed_response ,
116+ body , key_iteration_count ,
117+ trust_id )
118+ if not session :
119+ raise login_error (parsed_response )
120+ if trust_me :
121+ response = web_client .post ('https://lastpass.com/trust.php' ,
122+ cookies = {'PHPSESSID' : session .id },
123+ data = {
124+ "token" : session .token ,
125+ "uuid" : trust_id ,
126+ "trustlabel" : client_id ,
127+ })
128+
99129 return session
100130
101131
102- def create_session (parsed_response , key_iteration_count ):
132+ def oob_login (web_client , parsed_response , body , key_iteration_count , trust_id ):
133+ error = None if parsed_response .tag != 'response' else parsed_response .find (
134+ 'error' )
135+ if 'outofbandname' not in error .attrib or 'capabilities' not in error .attrib :
136+ return (None , parsed_response )
137+ oob_name = error .attrib ['outofbandname' ]
138+ oob_capabilities = error .attrib ['capabilities' ].split (',' )
139+ can_do_passcode = 'passcode' in oob_capabilities
140+ if can_do_passcode and 'outofband' not in oob_capabilities :
141+ return (None , parsed_response )
142+ body ['outofbandrequest' ] = '1'
143+ retries = 0
144+ # loop waiting for out of band approval, or failure
145+ while retries < 5 :
146+ retries += 1
147+ response = web_client .post ("https://lastpass.com/login.php" , data = body )
148+ if response .status_code != requests .codes .ok :
149+ raise NetworkError ()
150+
151+ try :
152+ parsed_response = etree .fromstring (response .content )
153+ except etree .ParseError :
154+ parsed_response = None
155+
156+ if parsed_response is None :
157+ raise InvalidResponseError ()
158+
159+ session = create_session (parsed_response , key_iteration_count , trust_id )
160+ if session :
161+ return (session , parsed_response )
162+ error = None if parsed_response .tag != 'response' else parsed_response .find (
163+ 'error' )
164+ if 'cause' in error .attrib and error .attrib ['cause' ] == 'outofbandrequired' :
165+ if 'retryid' in error .attrib :
166+ body ['outofbandretryid' ] = error .attrib ['retryid' ]
167+ body ['outofbandretry' ] = "1"
168+ continue
169+ return (None , parsed_response )
170+ return (None , parsed_response )
171+
172+
173+ def generate_trust_id ():
174+ return '' .join (random .choice (string .ascii_uppercase + string .digits + string .ascii_lowercase + "!@#$" ) for _ in range (32 ))
175+
176+
177+ def create_session (parsed_response , key_iteration_count , trust_id ):
103178 if parsed_response .tag == 'ok' :
104- session_id = parsed_response .attrib .get ('sessionid' )
179+ ok_response = parsed_response
180+ else :
181+ ok_response = parsed_response .find ("ok" )
182+ if ok_response is not None :
183+ session_id = ok_response .attrib .get ('sessionid' )
184+ token = ok_response .attrib .get ('token' )
105185 if isinstance (session_id , str ):
106- return Session (session_id , key_iteration_count )
186+ return Session (session_id , key_iteration_count , token , trust_id )
107187
108188
109189def login_error (parsed_response ):
@@ -117,6 +197,8 @@ def login_error(parsed_response):
117197 "googleauthrequired" : LastPassIncorrectGoogleAuthenticatorCodeError ,
118198 "googleauthfailed" : LastPassIncorrectGoogleAuthenticatorCodeError ,
119199 "yubikeyrestricted" : LastPassIncorrectYubikeyPasswordError ,
200+ "outofbandrequired" : LastPassIncorrectOutOfBandRequiredError ,
201+ "multifactorresponsefailed" : LastPassIncorrectMultiFactorResponseError ,
120202 }
121203
122204 cause = error .attrib .get ('cause' )
0 commit comments