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 ,
1619 LastPassUnknownError
1720)
1821from .session import Session
2124http = requests
2225
2326
24- def login (username , password , multifactor_password = None , client_id = None ):
27+ def login (username , password , multifactor_password = None , client_id = None , trust_id = None , trust_me = False ):
2528 key_iteration_count = request_iteration_count (username )
26- return request_login (username , password , key_iteration_count , multifactor_password , client_id )
29+ return request_login (username , password , key_iteration_count , multifactor_password , client_id , trust_id = trust_id , trust_me = trust_me )
2730
2831
2932def logout (session , web_client = http ):
@@ -63,21 +66,31 @@ def request_iteration_count(username, web_client=http):
6366 raise InvalidResponseError ('Key iteration count is not positive' )
6467
6568
66- def request_login (username , password , key_iteration_count , multifactor_password = None , client_id = None , web_client = http ):
69+ def request_login (username , password , key_iteration_count , multifactor_password = None , client_id = None , web_client = http , trust_id = None , trust_me = False ):
6770 body = {
68- 'method' : 'mobile' ,
69- 'web' : 1 ,
70- 'xml' : 1 ,
71+ 'method' : 'cli' ,
72+ 'xml' : 2 ,
7173 'username' : username ,
7274 'hash' : make_hash (username , password , key_iteration_count ),
7375 'iterations' : key_iteration_count ,
76+ 'includeprivatekeyenc' : 1 ,
77+ 'outofbandsupported' : 1
7478 }
7579
7680 if multifactor_password :
7781 body ['otp' ] = multifactor_password
7882
83+ # if client_id:
84+ # body['imei'] = client_id
85+
86+ if trust_me and not trust_id :
87+ trust_id = generate_trust_id ()
88+
89+ if trust_id :
90+ body ['uuid' ] = trust_id
91+
7992 if client_id :
80- body ['imei ' ] = client_id
93+ body ['trustlabel ' ] = client_id
8194
8295 response = web_client .post ('https://lastpass.com/login.php' ,
8396 data = body )
@@ -93,17 +106,84 @@ def request_login(username, password, key_iteration_count, multifactor_password=
93106 if parsed_response is None :
94107 raise InvalidResponseError ()
95108
96- session = create_session (parsed_response , key_iteration_count )
109+ session = create_session (parsed_response , key_iteration_count , trust_id )
97110 if not session :
98- raise login_error (parsed_response )
111+ try :
112+ raise login_error (parsed_response )
113+ except LastPassIncorrectOutOfBandRequiredError :
114+ (session , parsed_response ) = oob_login (web_client , parsed_response ,
115+ body , key_iteration_count ,
116+ trust_id )
117+ if not session :
118+ raise login_error (parsed_response )
119+ if trust_me :
120+ response = web_client .post ('https://lastpass.com/trust.php' ,
121+ cookies = {'PHPSESSID' : session .id },
122+ data = {
123+ "token" : session .token ,
124+ "uuid" : trust_id ,
125+ "trustlabel" : client_id ,
126+ })
127+
99128 return session
100129
101130
102- def create_session (parsed_response , key_iteration_count ):
131+ if trust_me :
132+ body ['trustlabel' ] = client_id
133+
134+
135+ def oob_login (web_client , parsed_response , body , key_iteration_count , trust_id ):
136+ error = None if parsed_response .tag != 'response' else parsed_response .find (
137+ 'error' )
138+ if 'outofbandname' not in error .attrib or 'capabilities' not in error .attrib :
139+ return (None , parsed_response )
140+ oob_name = error .attrib ['outofbandname' ]
141+ oob_capabilities = error .attrib ['capabilities' ].split (',' )
142+ can_do_passcode = 'passcode' in oob_capabilities
143+ if can_do_passcode and 'outofband' not in oob_capabilities :
144+ return (None , parsed_response )
145+ body ['outofbandrequest' ] = '1'
146+ # loop waiting for out of band approval, or failure
147+ while True :
148+ response = web_client .post ("https://lastpass.com/login.php" , data = body )
149+ if response .status_code != requests .codes .ok :
150+ raise NetworkError ()
151+
152+ try :
153+ parsed_response = etree .fromstring (response .content )
154+ except etree .ParseError :
155+ parsed_response = None
156+
157+ if parsed_response is None :
158+ raise InvalidResponseError ()
159+
160+ session = create_session (parsed_response , key_iteration_count , trust_id )
161+ if session :
162+ return (session , parsed_response )
163+ error = None if parsed_response .tag != 'response' else parsed_response .find (
164+ 'error' )
165+ if 'cause' in error .attrib and error .attrib ['cause' ] == 'outofbandrequired' :
166+ if 'retryid' in error .attrib :
167+ body ['outofbandretryid' ] = error .attrib ['retryid' ]
168+ body ['outofbandretry' ] = "1"
169+ continue
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,7 @@ def login_error(parsed_response):
117197 "googleauthrequired" : LastPassIncorrectGoogleAuthenticatorCodeError ,
118198 "googleauthfailed" : LastPassIncorrectGoogleAuthenticatorCodeError ,
119199 "yubikeyrestricted" : LastPassIncorrectYubikeyPasswordError ,
200+ "outofbandrequired" : LastPassIncorrectOutOfBandRequiredError ,
120201 }
121202
122203 cause = error .attrib .get ('cause' )
0 commit comments