@@ -16,22 +16,37 @@ def setUp(self):
1616
1717 self .hash = b'7880a04588cfab954aa1a2da98fd9c0d2c6eba4c53e36a94510e6dbf30759256'
1818 self .session_id = '53ru,Hb713QnEVM5zWZ16jMvxS0'
19- self .session = Session (self .session_id , self .key_iteration_count )
19+ self .token = '54aa1a2da98fd9c0d2c6eba4c5'
20+ self .session = Session (self .session_id , self .key_iteration_count , token = self .token )
2021
2122 self .blob_response = 'TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5'
2223 self .blob_bytes = b64decode (self .blob_response )
2324 self .blob = Blob (self .blob_bytes , self .key_iteration_count )
2425
25- self .login_post_data = {'method' : 'mobile' ,
26- 'web' : 1 ,
27- 'xml' : 1 ,
26+ self .login_post_data = {'method' : 'cli' ,
27+ 'xml' : 2 ,
28+ 'outofbandsupported' : 1 ,
29+ 'includeprivatekeyenc' : 1 ,
2830 'username' : self .username ,
2931 'hash' : self .hash ,
3032 'iterations' : self .key_iteration_count }
3133
3234 self .device_id = '492378378052455'
3335 self .login_post_data_with_device_id = self .login_post_data .copy ()
34- self .login_post_data_with_device_id .update ({'imei' : self .device_id })
36+ self .login_post_data_with_device_id .update ({'trustlabel' : self .device_id })
37+ self .login_post_data_with_device_id .update ({'uuid' : self .device_id })
38+
39+ self .trust_id = '@2ykJ0Tp#dVi06qh6g6kvzOqjQGAWfKv'
40+
41+ self .request_trust_data = {
42+ 'token' : self .token ,
43+ 'trustlabel' : self .device_id ,
44+ 'uuid' : self .trust_id
45+ }
46+
47+ self .request_trust_cookies = {
48+ 'PHPSESSID' : self .session_id
49+ }
3550
3651 self .google_authenticator_code = '12345'
3752 self .yubikey_password = 'emdbwzemyisymdnevznyqhqnklaqheaxszzvtnxjrmkb'
@@ -93,6 +108,9 @@ def test_request_login_makes_a_post_request(self):
93108 def test_request_login_makes_a_post_request_with_device_id (self ):
94109 self ._verify_request_login_post_request (None , self .device_id , self .login_post_data_with_device_id )
95110
111+ def test_request_login_makes_a_post_request_with_trust_requested (self ):
112+ self ._verify_request_trust (None , self .device_id , self .request_trust_data , cookies = self .request_trust_cookies , trust_id = self .trust_id , trust_me = True )
113+
96114 def test_request_login_makes_a_post_request_with_google_authenticator_code (self ):
97115 self ._verify_request_login_post_request (self .google_authenticator_code ,
98116 None ,
@@ -104,7 +122,8 @@ def test_request_login_makes_a_post_request_with_yubikey_password(self):
104122 self .login_post_data_with_yubikey_password )
105123
106124 def test_request_login_returns_a_session (self ):
107- self .assertEqual (self ._request_login_with_xml ('<ok sessionid="{}" />' .format (self .session_id )), self .session )
125+ tested_session = self ._request_login_with_xml ('<ok sessionid="{}" token="{}"/>' .format (self .session_id , self .token ))
126+ self .assertEqual (tested_session , self .session )
108127
109128 def test_request_login_raises_an_exception_on_http_error (self ):
110129 self .assertRaises (lastpass .NetworkError , self ._request_login_with_error )
@@ -147,6 +166,14 @@ def test_request_login_raises_an_exception_on_missing_or_incorrect_yubikey_passw
147166 self .assertRaises (lastpass .LastPassIncorrectYubikeyPasswordError ,
148167 self ._request_login_with_lastpass_error , 'yubikeyrestricted' , message )
149168
169+ def test_request_login_raises_an_exception_on_lastpass_authenticator (self ):
170+ message = 'Multifactor authentication required! ' \
171+ 'Upgrade your browser extension so you can enter it.'
172+ self .assertRaises (lastpass .LastPassIncorrectOutOfBandRequiredError ,
173+ self ._request_login_with_lastpass_multifactor_required , 'outofbandrequired' , message )
174+ self .assertRaises (lastpass .LastPassIncorrectMultiFactorResponseError ,
175+ self ._request_login_with_lastpass_multifactor_required , 'multifactorresponsefailed' , message )
176+
150177 def test_request_login_raises_an_exception_on_unknown_lastpass_error_without_a_message (self ):
151178 cause = 'Unknown cause'
152179 self .assertRaises (lastpass .LastPassUnknownError ,
@@ -162,7 +189,8 @@ def test_fetch_makes_a_get_request(self):
162189 def test_fetch_returns_a_blob (self ):
163190 m = mock .Mock ()
164191 m .get .return_value = self ._http_ok (self .blob_response )
165- self .assertEqual (fetcher .fetch (self .session , m ), self .blob )
192+ returned_blob = fetcher .fetch (self .session , m )
193+ self .assertEqual (returned_blob , self .blob )
166194
167195 def test_fetch_raises_exception_on_http_error (self ):
168196 m = mock .Mock ()
@@ -197,12 +225,18 @@ def test_make_hash(self):
197225 for iterations , hash in hashes :
198226 self .assertEqual (hash , fetcher .make_hash ('postlass@gmail.com' , 'pl1234567890' , iterations ))
199227
200- def _verify_request_login_post_request (self , multifactor_password , device_id , post_data ):
228+ def _verify_request_login_post_request (self , multifactor_password , device_id , post_data , trust_me = False ):
201229 m = mock .Mock ()
202230 m .post .return_value = self ._http_ok ('<ok sessionid="{}" />' .format (self .session_id ))
203- fetcher .request_login (self .username , self .password , self .key_iteration_count , multifactor_password , device_id , m )
231+ fetcher .request_login (self .username , self .password , self .key_iteration_count , multifactor_password , device_id , m , trust_id = device_id , trust_me = trust_me )
204232 m .post .assert_called_with ('https://lastpass.com/login.php' , data = post_data )
205233
234+ def _verify_request_trust (self , multifactor_password , device_id , post_data , cookies , trust_id , trust_me = False ):
235+ m = mock .Mock ()
236+ m .post .return_value = self ._http_ok ('<ok sessionid="{}" token="{}"/>' .format (self .session_id , self .token ))
237+ fetcher .request_login (self .username , self .password , self .key_iteration_count , multifactor_password , device_id , m , trust_id = trust_id , trust_me = trust_me )
238+ m .post .assert_called_with ('https://lastpass.com/trust.php' , data = post_data , cookies = cookies )
239+
206240 @staticmethod
207241 def _mock_response (code , body ):
208242 m = mock .Mock ()
@@ -222,9 +256,19 @@ def _lastpass_error(cause, message):
222256 return '<response><error cause="{}" message="{}" /></response>' .format (cause , message )
223257 return '<response><error cause="{}" /></response>' .format (cause )
224258
259+ @staticmethod
260+ def _lastpass_multifactor_required (cause , message ):
261+ if message :
262+ return '<response><error message="{}" cause="{}" allowtrust="1" capabilities="push,totp,sms,outofband,outofbandauto,passcode" outofbandtype="lastpassauth" outofbandname="LastPass Authenticator" allowmultifactortrust="true" trustexpired="0" trustlabel="" hidedisable="false" /></response>' .format (message , cause )
263+ return '<response><error cause="{}" allowtrust="1" capabilities="push,totp,sms,outofband,outofbandauto,passcode" outofbandtype="lastpassauth" outofbandname="LastPass Authenticator" allowmultifactortrust="true" trustexpired="0" trustlabel="" hidedisable="false" /></response>' .format (cause )
264+
225265 def _request_login_with_lastpass_error (self , cause , message = None ):
226266 return self ._request_login_with_xml (self ._lastpass_error (cause , message ))
227267
268+ def _request_login_with_lastpass_multifactor_required (self , cause , message = None ):
269+ return self ._request_login_with_xml (
270+ self ._lastpass_multifactor_required (cause , message ))
271+
228272 def _request_login_with_xml (self , text ):
229273 return self ._request_login_with_ok (text )
230274
0 commit comments