#!/bin/python3 import random, string, json, uuid, requests, hashlib from pprint import pprint BASE_URL = 'http://localhost:8090' AUTH_URL = BASE_URL + '/auth/full' REGISTER_URL = BASE_URL + '/signup' WALLET_STATE_URL = BASE_URL + '/wallet-state' def wallet_state_sequence(wallet_state): if 'deviceId' not in wallet_state: return 0 return wallet_state['lastSynced'][wallet_state['deviceId']] # TODO - do this correctly def create_login_password(root_password): return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[:32] # TODO - do this correctly def create_encryption_key(root_password): return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[32:] # TODO - do this correctly def check_hmac(wallet_state, encryption_key, hmac): return hmac == 'Good HMAC' # TODO - do this correctly def create_hmac(wallet_state, encryption_key): return 'Good HMAC' class Client(): def _validate_new_wallet_state(self, new_wallet_state): if self.wallet_state is None: # All of the validations here are in reference to what the device already # has. If this device is getting a wallet state for the first time, there # is no basis for comparison. return True # Make sure that the new sequence is overall later. if wallet_state_sequence(new_wallet_state) <= wallet_state_sequence(self.wallet_state): return False for dev_id in self.wallet_state['lastSynced']: if dev_id == self.device_id: # Check if the new wallet has the latest changes from this device if new_wallet_state['lastSynced'][dev_id] != self.wallet_state['lastSynced'][dev_id]: return False else: # Check if the new wallet somehow regressed on any of the other devices # This most likely means a bug in another client if new_wallet_state['lastSynced'][dev_id] < self.wallet_state['lastSynced'][dev_id]: return False return True def __init__(self): # Represents normal client behavior (though a real client will of course save device id) self.device_id = str(uuid.uuid4()) self.auth_token = 'bad token' self.wallet_state = None # TODO - save change to disk in between, associated with account and/or # wallet self._encrypted_wallet_local_changes = '' # TODO - make this act more sdk-like. in fact maybe even install the sdk? # TODO - This does not deal with the question of tying accounts to wallets. # Does a new wallet state mean a we're creating a new account? What happens # if we create a new wallet state tied to an existing account? Do we merge it # with what's on the server anyway? Do we refuse to merge, or warn the user? # Etc. This sort of depends on how the LBRY Desktop/SDK usually behave. For # now, it'll end up just merging any un-saved local changes with whatever is # on the server. def new_wallet_state(self): # camel-cased to ease json interop self.wallet_state = {'lastSynced': {}, 'encryptedWallet': ''} # TODO - actual encryption with encryption_key self._encrypted_wallet_local_changes = '' def set_account(self, email, root_password): self.email = email self.root_password = root_password def register(self): body = json.dumps({ 'email': self.email, 'password': create_login_password(self.root_password), }) response = requests.post(REGISTER_URL, body) if response.status_code != 201: print ('Error', response.status_code) print (response.content) return print ("Registered") def get_auth_token(self): body = json.dumps({ 'email': self.email, 'password': create_login_password(self.root_password), 'deviceId': self.device_id, }) response = requests.post(AUTH_URL, body) if response.status_code != 200: print ('Error', response.status_code) print (response.content) return self.auth_token = json.loads(response.content)['token'] print ("Got auth token: ", self.auth_token) # TODO - What about cases where we are managing multiple different wallets? # Some will have lower sequences. If you accidentally mix it up client-side, # you might end up overwriting one with a lower sequence entirely. Maybe we # want to annotate them with which account we're talking about. Again, we # should see how LBRY Desktop/SDK deal with it. def get_wallet_state(self): params = { 'token': self.auth_token, } response = requests.get(WALLET_STATE_URL, params=params) if response.status_code != 200: print ('Error', response.status_code) print (response.content) return new_wallet_state_str = json.loads(response.content)['walletStateJson'] new_wallet_state = json.loads(new_wallet_state_str) encryption_key = create_encryption_key(self.root_password) hmac = json.loads(response.content)['hmac'] if not check_hmac(new_wallet_state_str, encryption_key, hmac): print ('Error - bad hmac on new wallet') print (response.content) return # In reality, we'd examine, merge, verify, validate etc this new wallet state. if self.wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state): print ('Error - new wallet does not validate') print (response.content) return if self.wallet_state is None: # This is if we're getting a wallet_state for the first time. Initialize # the local changes. self._encrypted_wallet_local_changes = '' self.wallet_state = new_wallet_state print ("Got latest walletState:") pprint(self.wallet_state) def post_wallet_state(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.wallet_state to this until we know that it's accepted by # the server. if not self.wallet_state: print ("No wallet state to post.") return submitted_wallet_state = { "deviceId": self.device_id, "lastSynced": dict(self.wallet_state['lastSynced']), "encryptedWallet": self.cur_encrypted_wallet(), } submitted_wallet_state['lastSynced'][self.device_id] = wallet_state_sequence(self.wallet_state) + 1 encryption_key = create_encryption_key(self.root_password) submitted_wallet_state_str = json.dumps(submitted_wallet_state) submitted_wallet_state_hmac = create_hmac(submitted_wallet_state_str, encryption_key) body = json.dumps({ 'token': self.auth_token, 'walletStateJson': submitted_wallet_state_str, 'hmac': submitted_wallet_state_hmac }) response = requests.post(WALLET_STATE_URL, body) if response.status_code == 200: # Our local changes are no longer local, so we reset them self._encrypted_wallet_local_changes = '' print ('Successfully updated wallet state on server') elif response.status_code == 409: print ('Wallet state out of date. Getting updated wallet state. Try again.') # Don't return yet! We got the updated state here, so we still process it below. else: print ('Error', response.status_code) print (response.content) return # Now we get a new wallet state back as a response # TODO - factor this into the same thing as the get_wallet_state function new_wallet_state_str = json.loads(response.content)['walletStateJson'] new_wallet_state_hmac = json.loads(response.content)['hmac'] new_wallet_state = json.loads(new_wallet_state_str) if not check_hmac(new_wallet_state_str, encryption_key, new_wallet_state_hmac): print ('Error - bad hmac on new wallet') print (response.content) return # In reality, we'd examine, merge, verify, validate etc this new wallet state. if submitted_wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state): print ('Error - new wallet does not validate') print (response.content) return self.wallet_state = new_wallet_state print ("Got new walletState:") pprint(self.wallet_state) def change_encrypted_wallet(self): if not self.wallet_state: print ("No wallet state, so we can't add to it yet.") return self._encrypted_wallet_local_changes += ':' + ''.join(random.choice(string.hexdigits) for x in range(4)) def cur_encrypted_wallet(self): if not self.wallet_state: print ("No wallet state, so no encrypted wallet.") return # The local changes on top of whatever came from the server # If we pull new changes from server, we "rebase" these on top of it # If we push changes, the full "rebased" version gets committed to the server return self.wallet_state['encryptedWallet'] + self._encrypted_wallet_local_changes