From 231cb1b79b074ff25d2946ad0d9f2efef60215cc Mon Sep 17 00:00:00 2001
From: Daniel Krol <orblivion@gmail.com>
Date: Wed, 6 Jul 2022 17:55:15 -0400
Subject: [PATCH] Test client uses new password change endpoint

---
 test_client/test_client.py | 105 ++++++++++++++++++++++++++++++++++---
 1 file changed, 97 insertions(+), 8 deletions(-)

diff --git a/test_client/test_client.py b/test_client/test_client.py
index fcf089d..bd9db76 100755
--- a/test_client/test_client.py
+++ b/test_client/test_client.py
@@ -68,6 +68,7 @@ class WalletSync():
 
     self.AUTH_URL = API_URL + '/auth/full'
     self.REGISTER_URL = API_URL + '/signup'
+    self.PASSWORD_URL = API_URL + '/password'
     self.WALLET_URL = API_URL + '/wallet'
 
   def register(self, email, password):
@@ -122,7 +123,7 @@ class WalletSync():
 
   def update_wallet(self, wallet_state, hmac, token):
     body = json.dumps({
-      'token': token,
+      "token": token,
       "encryptedWallet": wallet_state.encrypted_wallet,
       "sequence": wallet_state.sequence,
       "hmac": hmac,
@@ -141,6 +142,49 @@ class WalletSync():
       print (response.content)
       raise Exception("Unexpected status code")
 
+  def change_password_with_wallet(self, wallet_state, hmac, email, old_password, new_password):
+    body = json.dumps({
+      "encryptedWallet": wallet_state.encrypted_wallet,
+      "sequence": wallet_state.sequence,
+      "hmac": hmac,
+      "email": email,
+      "oldPassword": old_password,
+      "newPassword": new_password,
+    })
+
+    response = requests.post(self.PASSWORD_URL, body)
+
+    if response.status_code == 200:
+      print ('Successfully updated password and wallet state on server')
+      return True
+    elif response.status_code == 409:
+      print ('Either submitted wallet is out of date, or there was no wallet on the server to update in the first place.')
+      return False
+    else:
+      print ('Error', response.status_code)
+      print (response.content)
+      raise Exception("Unexpected status code")
+
+  def change_password_no_wallet(self, email, old_password, new_password):
+    body = json.dumps({
+      "email": email,
+      "oldPassword": old_password,
+      "newPassword": new_password,
+    })
+
+    response = requests.post(self.PASSWORD_URL, body)
+
+    if response.status_code == 200:
+      print ('Successfully updated password on server')
+      return True
+    elif response.status_code == 409:
+      print ('There is a wallet on the server that needs to be updated with password change.')
+      return False
+    else:
+      print ('Error', response.status_code)
+      print (response.content)
+      raise Exception("Unexpected status code")
+
 def derive_secrets(root_password, salt):
     # TODO - Audit me audit me audit me! I don't know if these values are
     # optimal.
@@ -163,6 +207,7 @@ def derive_secrets(root_password, salt):
     key_length = 32
     num_keys = 3
 
+    print ("Generating keys...")
     kdf_output = scrypt(
       bytes(root_password, 'utf-8'),
       salt=salt,
@@ -172,6 +217,7 @@ def derive_secrets(root_password, salt):
       p=scrypt_p,
       maxmem=1100000000, # TODO - is this a lot?
     )
+    print ("Done generating keys")
 
     # Split the output in three
     parts = (
@@ -347,16 +393,15 @@ class Client():
 
   # Returns: status
   def update_remote_wallet(self):
-    # Create a *new* wallet state, indicating that it was last updated by this
-    # device, with the updated sequence, and include our local encrypted wallet changes.
-    # Don't set self.synced_wallet_state to this until we know that it's accepted by
-    # the server.
+    # Create a *new* wallet state, with the updated sequence, and include our
+    # local encrypted wallet changes. Don't set self.synced_wallet_state to
+    # this until we know that it's accepted by the server.
     if not self.synced_wallet_state:
       print ("No wallet state to post.")
       return "Error"
 
     submitted_wallet_state = WalletState(
-      encrypted_wallet=self.get_local_encrypted_wallet(),
+      encrypted_wallet=self.get_local_encrypted_wallet(self.sync_password),
       sequence=self.synced_wallet_state.sequence + 1
     )
     hmac = create_hmac(submitted_wallet_state, self.hmac_key)
@@ -375,6 +420,50 @@ class Client():
     print ("Could not update. Need to get new wallet and merge")
     return "Failure"
 
+  # Returns: status
+  def change_password(self, new_root_password):
+    # Change the password on the server. If a wallet exists on the server,
+    # update that as well so that the sync password and hmac key are derived
+    # from the same root password as the lbry id password.
+
+    new_lbry_id_password, new_sync_password, new_hmac_key = derive_secrets(new_root_password, self.salt)
+
+    if self.synced_wallet_state and self.synced_wallet_state.sequence > 0:
+      # Create a *new* wallet state (with our new sync password), with the
+      # updated sequence, and include our local encrypted wallet changes.
+      # Don't set self.synced_wallet_state to this until we know that it's
+      # accepted by the server.
+
+      submitted_wallet_state = WalletState(
+        encrypted_wallet=self.get_local_encrypted_wallet(new_sync_password),
+        sequence=self.synced_wallet_state.sequence + 1
+      )
+      hmac = create_hmac(submitted_wallet_state, new_hmac_key)
+
+      # Update our password and submit our wallet.
+      updated = self.wallet_sync_api.change_password_with_wallet(submitted_wallet_state, hmac, self.email, self.lbry_id_password, new_lbry_id_password)
+
+      if updated:
+        # We updated it. Now it's synced and we mark it as such. Update everything in one command to keep local changes in sync!
+        self.synced_wallet_state, self.lbry_id_password, self.sync_password, self.hmac_key = (
+          submitted_wallet_state, new_lbry_id_password, new_sync_password, new_hmac_key)
+
+        print ("Synced walletState:")
+        pprint(self.synced_wallet_state)
+        return "Success"
+    else:
+      # Update our password.
+      updated = self.wallet_sync_api.change_password_no_wallet(self.email, self.lbry_id_password, new_lbry_id_password)
+
+      if updated:
+        # We updated it. Now we mark it as such. Update everything in one command to keep local changes in sync!
+        self.lbry_id_password, self.sync_password, self.hmac_key = (
+          new_lbry_id_password, new_sync_password, new_hmac_key)
+        return "Success"
+
+    print ("Could not update. Need to get new wallet and merge")
+    return "Failure"
+
   def set_preference(self, key, value):
     # TODO - error checking
     return LBRYSDK.set_preference(self.wallet_id, key, value)
@@ -387,6 +476,6 @@ class Client():
     # TODO - error checking
     return LBRYSDK.update_wallet(self.wallet_id, self.sync_password, encrypted_wallet)
 
-  def get_local_encrypted_wallet(self):
+  def get_local_encrypted_wallet(self, sync_password):
     # TODO - error checking
-    return LBRYSDK.get_wallet(self.wallet_id, self.sync_password)
+    return LBRYSDK.get_wallet(self.wallet_id, sync_password)