2021-12-25 02:16:58 +01:00
#!/bin/python3
2022-06-09 23:04:49 +02:00
from collections import namedtuple
2022-06-16 23:21:57 +02:00
import base64 , json , uuid , requests , hashlib , hmac
2022-06-07 19:25:14 +02:00
from pprint import pprint
2022-07-15 21:36:11 +02:00
from hashlib import scrypt , sha256 # TODO - audit! Should I use hazmat `Scrypt` instead for some reason?
import secrets
2022-08-27 17:37:09 +02:00
import threading
2021-12-25 02:16:58 +01:00
2022-06-11 03:07:55 +02:00
WalletState = namedtuple ( ' WalletState ' , [ ' sequence ' , ' encrypted_wallet ' ] )
2022-08-27 17:37:09 +02:00
import asyncio , time
from websockets import connect as websockets_connect
2022-06-14 02:42:56 +02:00
class LBRYSDK ( ) :
@staticmethod
def get_wallet ( wallet_id , password ) :
response = requests . post ( ' http://localhost:5279 ' , json . dumps ( {
" method " : " sync_apply " ,
" params " : {
" password " : password ,
" wallet_id " : wallet_id ,
} ,
} ) )
return response . json ( ) [ ' result ' ] [ ' data ' ]
2022-07-08 18:36:10 +02:00
@staticmethod
def get_hash ( wallet_id ) :
response = requests . post ( ' http://localhost:5279 ' , json . dumps ( {
" method " : " sync_hash " ,
" params " : {
" wallet_id " : wallet_id ,
} ,
} ) )
return response . json ( ) [ ' result ' ]
2022-06-14 02:42:56 +02:00
@staticmethod
def update_wallet ( wallet_id , password , data ) :
response = requests . post ( ' http://localhost:5279 ' , json . dumps ( {
" method " : " sync_apply " ,
" params " : {
" data " : data ,
" password " : password ,
" wallet_id " : wallet_id ,
} ,
} ) )
return response . json ( ) [ ' result ' ] [ ' data ' ]
@staticmethod
2022-06-16 04:21:00 +02:00
def set_preference ( wallet_id , key , value ) :
2022-06-14 02:42:56 +02:00
response = requests . post ( ' http://localhost:5279 ' , json . dumps ( {
" method " : " preference_set " ,
" params " : {
" key " : key ,
" value " : value ,
2022-06-16 04:21:00 +02:00
" wallet_id " : wallet_id ,
2022-06-14 02:42:56 +02:00
} ,
} ) )
return response . json ( ) [ ' result ' ]
@staticmethod
2022-06-16 04:21:00 +02:00
def get_preferences ( wallet_id ) :
2022-06-14 02:42:56 +02:00
response = requests . post ( ' http://localhost:5279 ' , json . dumps ( {
" method " : " preference_get " ,
2022-06-16 04:21:00 +02:00
" params " : {
" wallet_id " : wallet_id ,
} ,
2022-06-14 02:42:56 +02:00
} ) )
return response . json ( ) [ ' result ' ]
2022-06-11 03:07:55 +02:00
class WalletSync ( ) :
2022-06-16 17:46:29 +02:00
def __init__ ( self , local ) :
2022-07-15 21:36:11 +02:00
self . API_VERSION = 3
2022-06-16 23:58:11 +02:00
2022-06-16 17:46:29 +02:00
if local :
2022-08-27 17:37:09 +02:00
BASE_HTTP_URL = ' http://localhost:8090 '
BASE_WS_URL = ' ws://localhost:8090 '
2022-06-16 17:46:29 +02:00
else :
2022-08-27 17:37:09 +02:00
BASE_HTTP_URL = ' https://dev.lbry.id '
BASE_WS_URL = ' wss://dev.lbry.id '
2022-07-15 21:36:11 +02:00
# Avoid confusion. I sometimes forget, at any rate.
2022-08-27 17:37:09 +02:00
print ( " Connecting to Wallet API at " + BASE_HTTP_URL )
API_HTTP_URL = BASE_HTTP_URL + ' /api/ %d ' % self . API_VERSION
API_WS_URL = BASE_WS_URL + ' /api/ %d ' % self . API_VERSION
2022-07-15 21:36:11 +02:00
2022-08-27 17:37:09 +02:00
self . AUTH_URL = API_HTTP_URL + ' /auth/full '
self . REGISTER_URL = API_HTTP_URL + ' /signup '
self . PASSWORD_URL = API_HTTP_URL + ' /password '
self . WALLET_URL = API_HTTP_URL + ' /wallet '
self . CLIENT_SALT_SEED_URL = API_HTTP_URL + ' /client-salt-seed '
2022-06-11 03:07:55 +02:00
2022-08-27 17:37:09 +02:00
self . WEBSOCKET_URL = API_WS_URL + ' /websocket '
2022-06-16 20:11:11 +02:00
2022-08-14 01:37:54 +02:00
# def resend_registration_email():
# also rename this to __init__.py later
2022-07-15 21:36:11 +02:00
def register ( self , email , password , salt_seed ) :
2022-06-11 03:07:55 +02:00
body = json . dumps ( {
' email ' : email ,
' password ' : password ,
2022-07-15 21:36:11 +02:00
' clientSaltSeed ' : salt_seed ,
2022-06-11 03:07:55 +02:00
} )
2022-06-16 17:46:29 +02:00
response = requests . post ( self . REGISTER_URL , body )
2022-06-11 03:07:55 +02:00
if response . status_code != 201 :
print ( ' Error ' , response . status_code )
print ( response . content )
return False
return True
2022-06-16 17:46:29 +02:00
def get_auth_token ( self , email , password , device_id ) :
2022-06-11 03:07:55 +02:00
body = json . dumps ( {
' email ' : email ,
' password ' : password ,
' deviceId ' : device_id ,
} )
2022-06-16 17:46:29 +02:00
response = requests . post ( self . AUTH_URL , body )
2022-06-11 03:07:55 +02:00
if response . status_code != 200 :
print ( ' Error ' , response . status_code )
print ( response . content )
return None
return response . json ( ) [ ' token ' ]
2022-07-15 21:36:11 +02:00
def get_salt_seed ( self , email ) :
params = {
2022-08-27 17:37:09 +02:00
' email ' : base64 . encodebytes ( bytes ( email . encode ( ' utf-8 ' ) ) ) ,
2022-07-15 21:36:11 +02:00
}
response = requests . get ( self . CLIENT_SALT_SEED_URL , params = params )
if response . status_code == 404 :
print ( ' Account not found ' )
raise Exception ( " Account not found " )
if response . status_code != 200 :
print ( ' Error ' , response . status_code )
print ( response . content )
raise Exception ( " Unexpected status code " )
return response . json ( ) [ ' clientSaltSeed ' ]
2022-06-16 17:46:29 +02:00
def get_wallet ( self , token ) :
2022-06-11 03:07:55 +02:00
params = {
' token ' : token ,
}
2022-06-16 17:46:29 +02:00
response = requests . get ( self . WALLET_URL , params = params )
2022-06-11 03:07:55 +02:00
2022-06-14 16:58:45 +02:00
if response . status_code == 404 :
print ( ' Wallet not found ' )
# "No wallet" is not an error, so no exception raised
return None , None
2022-06-11 03:07:55 +02:00
if response . status_code != 200 :
print ( ' Error ' , response . status_code )
print ( response . content )
2022-06-14 16:58:45 +02:00
raise Exception ( " Unexpected status code " )
2022-06-11 03:07:55 +02:00
wallet_state = WalletState (
encrypted_wallet = response . json ( ) [ ' encryptedWallet ' ] ,
sequence = response . json ( ) [ ' sequence ' ] ,
)
hmac = response . json ( ) [ ' hmac ' ]
return wallet_state , hmac
2022-06-16 17:46:29 +02:00
def update_wallet ( self , wallet_state , hmac , token ) :
2022-06-11 03:07:55 +02:00
body = json . dumps ( {
2022-07-06 23:55:15 +02:00
" token " : token ,
2022-06-11 03:07:55 +02:00
" encryptedWallet " : wallet_state . encrypted_wallet ,
" sequence " : wallet_state . sequence ,
" hmac " : hmac ,
} )
2022-06-16 17:46:29 +02:00
response = requests . post ( self . WALLET_URL , body )
2022-06-11 03:07:55 +02:00
if response . status_code == 200 :
print ( ' Successfully updated wallet state on server ' )
2022-06-23 21:22:31 +02:00
return True
2022-06-11 03:07:55 +02:00
elif response . status_code == 409 :
2022-06-23 21:22:31 +02:00
print ( ' Submitted wallet is out of date. ' )
return False
2022-06-11 03:07:55 +02:00
else :
print ( ' Error ' , response . status_code )
print ( response . content )
2022-06-14 16:58:45 +02:00
raise Exception ( " Unexpected status code " )
2022-06-11 03:07:55 +02:00
2022-07-15 21:36:11 +02:00
def change_password_with_wallet ( self , wallet_state , hmac , email , old_password , new_password , salt_seed ) :
2022-07-06 23:55:15 +02:00
body = json . dumps ( {
" encryptedWallet " : wallet_state . encrypted_wallet ,
" sequence " : wallet_state . sequence ,
" hmac " : hmac ,
" email " : email ,
" oldPassword " : old_password ,
" newPassword " : new_password ,
2022-07-15 21:36:11 +02:00
' clientSaltSeed ' : salt_seed ,
2022-07-06 23:55:15 +02:00
} )
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 " )
2022-07-15 21:36:11 +02:00
def change_password_no_wallet ( self , email , old_password , new_password , salt_seed ) :
2022-07-06 23:55:15 +02:00
body = json . dumps ( {
" email " : email ,
" oldPassword " : old_password ,
" newPassword " : new_password ,
2022-07-15 21:36:11 +02:00
' clientSaltSeed ' : salt_seed ,
2022-07-06 23:55:15 +02:00
} )
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 " )
2022-08-27 17:37:09 +02:00
# NOTE this doesn't have a way to explicitly disconnect! Hopefully the real
# thing is designed better.
# NOTE - if you change your password, the server will kick off all existing
# websocket connections for that user. each client will need to change their
# password to connect again.
def start_websocket ( self , client_name , token ) :
DEBUG = False
# Poor man's debug log
debugLog = lambda * x : print ( * x ) if DEBUG else None
self . try_connect_websocket = True
async def connection ( ) :
while self . try_connect_websocket :
debugLog ( client_name , " trying to connect " )
try :
async with websockets_connect ( self . WEBSOCKET_URL + " ?token= " + token ) as websocket :
print ( client_name , " connected for now " )
while True :
try :
msg = await websocket . recv ( )
# ex: 'wallet-update:5'
if msg . startswith ( ' wallet-update ' ) :
sequence = int ( msg . split ( ' : ' ) [ - 1 ] )
print ( client_name , " got notified of a wallet update, sequence= " + str ( sequence ) + " . If your client is behind this sequence, you should get the latest from the server. " )
else :
debugLog ( client_name , " got an unknown message: " , msg )
except Exception as e :
print ( client_name , " disconnected for now: " , e )
time . sleep ( 1 )
break
except Exception as e :
debugLog ( client_name , " failed to connect: " , e )
time . sleep ( 1 )
asyncio . run ( connection ( ) )
# NOTE - this only stops retrying connections and sending messages. If a
# socket is happily connected this won't stop it.
def stop_try_reconnect_websocket ( self ) :
self . try_connect_websocket = False
2022-07-15 21:36:11 +02:00
# Thanks to Standard Notes. See:
# https://docs.standardnotes.com/specification/encryption/
# Sized in bytes
SALT_SEED_LENGTH = 32
SALT_LENGTH = 16
def generate_salt_seed ( ) :
return secrets . token_hex ( SALT_SEED_LENGTH )
def generate_salt ( email , seed ) :
hash_input = ( email + " : " + seed ) . encode ( ' utf-8 ' )
hash_output = sha256 ( hash_input ) . hexdigest ( ) . encode ( ' utf-8 ' )
return bytes ( hash_output [ : ( SALT_LENGTH * 2 ) ] )
def derive_secrets ( root_password , email , salt_seed ) :
2022-07-13 12:44:42 +02:00
# 2017 Scrypt parameters: https://words.filippo.io/the-scrypt-parameters/
2022-06-22 18:02:48 +02:00
#
2022-07-13 12:44:42 +02:00
# There's recommendations for interactive use, and stronger recommendations
# for sensitive storage. Going with the latter since we're storing
# encrypted stuff on a server.
2022-07-15 21:36:11 +02:00
#
# Auditors double check.
2022-06-22 18:02:48 +02:00
scrypt_n = 1 << 20
scrypt_r = 8
2022-06-16 23:06:55 +02:00
scrypt_p = 1
key_length = 32
2022-08-08 22:31:29 +02:00
num_keys = 2
2022-06-16 23:06:55 +02:00
2022-07-15 21:36:11 +02:00
salt = generate_salt ( email , salt_seed )
2022-07-06 23:55:15 +02:00
print ( " Generating keys... " )
2022-06-22 22:41:08 +02:00
kdf_output = scrypt (
bytes ( root_password , ' utf-8 ' ) ,
salt = salt ,
dklen = key_length * num_keys ,
2022-06-16 23:06:55 +02:00
n = scrypt_n ,
r = scrypt_r ,
p = scrypt_p ,
2022-06-22 22:41:08 +02:00
maxmem = 1100000000 , # TODO - is this a lot?
2022-06-16 23:06:55 +02:00
)
2022-07-06 23:55:15 +02:00
print ( " Done generating keys " )
2021-12-25 02:16:58 +01:00
2022-06-16 23:06:55 +02:00
# Split the output in three
parts = (
kdf_output [ : key_length ] ,
kdf_output [ key_length : key_length * 2 ] ,
)
2022-06-07 19:25:14 +02:00
2022-06-28 03:16:07 +02:00
lbry_id_password = base64 . b64encode ( parts [ 0 ] ) . decode ( ' utf-8 ' )
2022-08-08 22:31:29 +02:00
hmac_key = parts [ 1 ]
2022-06-16 23:06:55 +02:00
2022-08-08 22:31:29 +02:00
return lbry_id_password , hmac_key
2022-06-07 19:25:14 +02:00
2022-06-09 23:04:49 +02:00
def create_hmac ( wallet_state , hmac_key ) :
2022-06-16 23:21:57 +02:00
input_str = str ( wallet_state . sequence ) + ' : ' + wallet_state . encrypted_wallet
return hmac . new ( hmac_key , input_str . encode ( ' utf-8 ' ) , hashlib . sha256 ) . hexdigest ( )
2022-06-07 19:25:14 +02:00
2022-06-09 23:04:49 +02:00
def check_hmac ( wallet_state , hmac_key , hmac ) :
return hmac == create_hmac ( wallet_state , hmac_key )
2021-12-25 02:16:58 +01:00
class Client ( ) :
2022-06-10 21:05:07 +02:00
# If you want to get the lastSynced stuff back, see:
# 512ebe3e95bf4e533562710a7f91c59616a9a197
# It's mostly simple, but the _validate_new_wallet_state changes may be worth
# looking at.
2021-12-25 02:16:58 +01:00
def _validate_new_wallet_state ( self , new_wallet_state ) :
2022-06-13 02:37:18 +02:00
if self . synced_wallet_state is None :
2021-12-25 02:16:58 +01:00
# 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.
2022-06-13 02:37:18 +02:00
if new_wallet_state . sequence < = self . synced_wallet_state . sequence :
2021-12-25 02:16:58 +01:00
return False
return True
2022-08-27 17:37:09 +02:00
def __init__ ( self , client_name , email , root_password , wallet_id = ' default_wallet ' , local = False ) :
2022-07-15 21:36:11 +02:00
self . wallet_sync_api = WalletSync ( local = local )
2022-08-27 17:37:09 +02:00
self . client_name = client_name # Just for async output so we know who's talking
2022-07-15 21:36:11 +02:00
2021-12-25 02:16:58 +01:00
# Represents normal client behavior (though a real client will of course save device id)
self . device_id = str ( uuid . uuid4 ( ) )
2022-08-27 17:37:09 +02:00
self . auth_token = ' bad-token '
2022-06-13 02:37:18 +02:00
self . synced_wallet_state = None
2021-12-25 02:16:58 +01:00
2022-06-14 23:26:57 +02:00
self . email = email
2022-07-15 21:36:11 +02:00
self . root_password = root_password
2022-06-16 23:06:55 +02:00
2022-06-14 23:26:57 +02:00
self . wallet_id = wallet_id
2022-06-07 19:25:14 +02:00
2022-08-27 17:37:09 +02:00
self . ws_thread = None
2022-07-15 21:36:11 +02:00
def register ( self ) :
# Note that for each registration, i.e. for each domain, we generate a
# different salt seed.
2022-07-22 22:37:27 +02:00
#
# Auditor - Does changing salt seed here cover the threat of sync servers
# guessing the password of the same user on another sync server? It should
# be a new seed if it's a new server.
2022-07-15 21:36:11 +02:00
self . salt_seed = generate_salt_seed ( )
2022-08-08 22:31:29 +02:00
self . lbry_id_password , self . hmac_key = derive_secrets (
2022-07-15 21:36:11 +02:00
self . root_password , self . email , self . salt_seed )
success = self . wallet_sync_api . register (
self . email ,
self . lbry_id_password ,
self . salt_seed
)
if success :
print ( " Registered " )
2022-06-16 17:46:29 +02:00
2022-07-07 18:33:18 +02:00
def set_local_password ( self , root_password ) :
2022-07-15 21:36:11 +02:00
"""
For clients to catch up to another client that just changed the password .
"""
2022-07-07 18:33:18 +02:00
# TODO - is UTF-8 appropriate for root_password? based on characters used etc.
2022-07-15 21:36:11 +02:00
self . root_password = root_password
2022-08-09 23:46:11 +02:00
self . update_derived_secrets ( )
2022-07-15 21:36:11 +02:00
2022-08-09 23:46:11 +02:00
def update_derived_secrets ( self ) :
2022-07-15 21:36:11 +02:00
"""
For clients other than the one that most recently registered or changed the
password , use this to get the salt seed from the server and generate keys
locally .
"""
self . salt_seed = self . wallet_sync_api . get_salt_seed ( self . email )
2022-08-08 22:31:29 +02:00
self . lbry_id_password , self . hmac_key = derive_secrets (
2022-07-15 21:36:11 +02:00
self . root_password , self . email , self . salt_seed )
2022-07-07 18:33:18 +02:00
2022-06-07 19:25:14 +02:00
# 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.
2021-12-25 02:16:58 +01:00
2022-06-14 23:26:57 +02:00
# TODO - Later, we should be saving the synced_wallet_state to disk, or
# something like that, so we know whether there are unpushed changes on
# startup (which should be uncommon but possible if crash or network problem
# in previous run). This will be important when the client is responsible for
# merging what comes from the server with those local unpushed changes. For
# now, the SDK handles merges with timestamps and such so it's as safe as
# always to just merge in.
2022-06-23 21:22:31 +02:00
# TODO - Save wallet state to disk, and init by pulling from disk. That way,
# we'll know what the merge base is, and we won't have to merge from 0 each
# time the app restarts.
# TODO - Wrap this back into __init__, now that I got the empty encrypted
# wallet right.
2022-06-14 23:26:57 +02:00
def init_wallet_state ( self ) :
2022-06-23 21:22:31 +02:00
# Represents what's been synced to the wallet sync server. It starts with
# sequence=0 which means nothing has been synced yet. As such, we start
# with an empty encrypted_wallet here. Anything currently in the SDK is a
# local-only change until it's pushed. If there's a merge conflict,
# sequence=0, empty encrypted_wallet will be the merge base. That way we
# won't lose any changes.
self . synced_wallet_state = WalletState (
sequence = 0 ,
2022-07-08 18:36:10 +02:00
# TODO - This should be the encrypted form of the empty wallet. The very
# first baseline, which could be used for merges in weird cases where
# users make conflicting changes on two different clients before ever
# pushing to the sync server.
2022-06-23 21:22:31 +02:00
encrypted_wallet = " " ,
)
2022-07-08 18:36:10 +02:00
# Initialize to the hash of the empty wallet. This way we will know if any
# changes to the wallet exist that haven't been pushed yet, even if the
# changes were made before the wallet state was initialized.
# TODO - actually set the right hash
self . mark_local_changes_synced_to_empty ( )
2021-12-25 02:16:58 +01:00
2022-08-27 17:37:09 +02:00
def start_websocket ( self ) :
# NOTE - Not putting any effort into here responsible thread programming
# here. Not accounting for errors, logging out and logging into other
# servers, etc. Only going so far as to make sure we don't start two at
# once.
if self . ws_thread is None :
self . ws_thread = threading . Thread (
target = self . wallet_sync_api . start_websocket ,
args = ( self . client_name , self . auth_token ) ,
daemon = True ,
)
self . ws_thread . start ( )
else :
print ( " Websocket already connected (or trying to). " )
def stop_try_reconnect_websocket ( self ) :
self . wallet_sync_api . stop_try_reconnect_websocket ( )
# Not trying to be a responsible thread programmer here, this is just a
# demo, and not a threading demo
self . ws_thread = None
2022-06-08 00:24:01 +02:00
def get_auth_token ( self ) :
2022-06-16 17:46:29 +02:00
token = self . wallet_sync_api . get_auth_token (
2022-06-11 03:07:55 +02:00
self . email ,
2022-06-28 03:16:07 +02:00
self . lbry_id_password ,
2022-06-11 03:07:55 +02:00
self . device_id ,
)
if not token :
2022-07-29 20:33:19 +02:00
# In a real client, this is where you may consider
# a) Offering to have the user change their password
2022-08-09 23:46:11 +02:00
# b) Try update_derived_secrets() and get_auth_token() silently, for the unlikely case that the user changed their password back and forth
2022-08-01 17:50:16 +02:00
print ( " Failed to get the auth token. Do you need to verify your email address? Or update this client ' s password (set_local_password())? " )
2022-08-09 23:46:11 +02:00
print ( " Or, in the off-chance the user changed their password back and forth, try updating secrets (update_derived_secrets()) to get the latest salt seed. " )
2021-12-25 02:16:58 +01:00
return
2022-06-11 03:07:55 +02:00
self . auth_token = token
2021-12-25 02:16:58 +01:00
print ( " Got auth token: " , self . auth_token )
2022-06-07 19:25:14 +02:00
# TODO - What about cases where we are managing multiple different wallets?
# Some will have lower sequences. If you accidentally mix it up client-side,
2022-06-14 23:26:57 +02:00
# you might end up overwriting one wallet with another if the former has a
# higher sequence number. Maybe we want to annotate them with which account
# we're talking about. Again, we should see how LBRY Desktop/SDK deal with
# it.
2022-06-23 21:22:31 +02:00
def get_merged_wallet_state ( self , new_wallet_state ) :
# Eventually, we will look for local changes in
# `get_local_encrypted_wallet()` by comparing it to
# `self.synced_wallet_state.encrypted_wallet`.
#
# If there are no local changes, we can just return `new_wallet_state`.
#
# If there are local changes, we will merge between `new_wallet_state` and
# `get_local_encrypted_wallet()`, using
# `self.synced_wallet_state.encrypted_wallet` as our merge base.
#
# For really hairy cases, this could even be a whole interactive process,
# not just a function.
# For now, the SDK handles merging (in a way that we hope to improve with
2022-07-08 18:36:10 +02:00
# the above eventually) so we will just return `new_wallet_state`. However,
# since we can at least compare hashes, we'll leave a little note for the
# user indicating that we're doing a merge. Caveat: We can't do it on
# sequence=0 because we can't get a sense of whether changes were made on a
# client before the first sync.
if self . synced_wallet_state . sequence > 0 :
if self . has_unsynced_local_changes ( ) :
print ( " Merging local changes with remote changes to create latest walletState. " )
else :
print ( " Nothing to merge. Taking remote walletState as latest walletState. " )
2022-06-23 21:22:31 +02:00
return new_wallet_state
2022-06-14 23:26:57 +02:00
# Returns: status
def get_remote_wallet ( self ) :
2022-07-07 18:33:18 +02:00
try :
new_wallet_state , hmac = self . wallet_sync_api . get_wallet ( self . auth_token )
except Exception :
return " Failed to get remote wallet "
2022-06-11 03:07:55 +02:00
if not new_wallet_state :
2022-06-14 23:26:57 +02:00
# Wallet not found, but this is not an error
return " Not Found "
2021-12-25 02:16:58 +01:00
2022-06-16 23:06:55 +02:00
if not check_hmac ( new_wallet_state , self . hmac_key , hmac ) :
2022-06-07 19:25:14 +02:00
print ( ' Error - bad hmac on new wallet ' )
2022-06-11 03:07:55 +02:00
print ( new_wallet_state , hmac )
2022-06-14 23:26:57 +02:00
return " Error "
2021-12-25 02:16:58 +01:00
2022-06-13 02:37:18 +02:00
if self . synced_wallet_state != new_wallet_state and not self . _validate_new_wallet_state ( new_wallet_state ) :
2021-12-25 02:16:58 +01:00
print ( ' Error - new wallet does not validate ' )
2022-06-13 02:37:18 +02:00
print ( ' current: ' , self . synced_wallet_state )
2022-06-11 03:07:55 +02:00
print ( ' got: ' , new_wallet_state )
2022-06-14 23:26:57 +02:00
return " Error "
2022-06-07 19:25:14 +02:00
2022-06-23 21:22:31 +02:00
merged_wallet_state = self . get_merged_wallet_state ( new_wallet_state )
2021-12-25 02:16:58 +01:00
2022-06-23 21:22:31 +02:00
# TODO error recovery between these two steps? sequence of events?
# This isn't gonna be quite right. Look at state diagrams.
self . synced_wallet_state = merged_wallet_state
self . update_local_encrypted_wallet ( merged_wallet_state . encrypted_wallet )
2022-07-08 18:36:10 +02:00
# We just took the value from the sync server, so local changes are synced
self . mark_local_changes_synced ( )
print ( " Got latest walletState: " )
2022-06-13 02:37:18 +02:00
pprint ( self . synced_wallet_state )
2022-06-14 23:26:57 +02:00
return " Success "
2021-12-25 02:16:58 +01:00
2022-06-14 23:26:57 +02:00
# Returns: status
def update_remote_wallet ( self ) :
2022-07-06 23:55:15 +02:00
# 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.
2022-06-13 02:37:18 +02:00
if not self . synced_wallet_state :
2022-06-07 19:25:14 +02:00
print ( " No wallet state to post. " )
2022-06-14 23:26:57 +02:00
return " Error "
2022-06-07 19:25:14 +02:00
2022-06-09 23:04:49 +02:00
submitted_wallet_state = WalletState (
2022-08-08 22:31:29 +02:00
encrypted_wallet = self . get_local_encrypted_wallet ( self . root_password ) ,
2022-06-13 02:37:18 +02:00
sequence = self . synced_wallet_state . sequence + 1
2022-06-09 23:04:49 +02:00
)
2022-06-16 23:06:55 +02:00
hmac = create_hmac ( submitted_wallet_state , self . hmac_key )
2022-06-09 23:04:49 +02:00
2022-06-23 21:22:31 +02:00
# Submit our wallet.
updated = self . wallet_sync_api . update_wallet ( submitted_wallet_state , hmac , self . auth_token )
2021-12-25 02:16:58 +01:00
2022-06-23 21:22:31 +02:00
if updated :
# We updated it. Now it's synced and we mark it as such.
self . synced_wallet_state = submitted_wallet_state
2022-06-13 02:37:18 +02:00
2022-07-08 18:36:10 +02:00
# We just pushed our local changes to the server, so local changes are synced
self . mark_local_changes_synced ( )
2022-06-23 21:22:31 +02:00
print ( " Synced walletState: " )
pprint ( self . synced_wallet_state )
return " Success "
2021-12-25 02:16:58 +01:00
2022-06-23 21:22:31 +02:00
print ( " Could not update. Need to get new wallet and merge " )
return " Failure "
2021-12-25 02:16:58 +01:00
2022-07-06 23:55:15 +02:00
# 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.
2022-07-22 22:37:27 +02:00
# Auditor - Should we be generating a *new* seed for every password change?
2022-07-15 21:36:11 +02:00
self . salt_seed = generate_salt_seed ( )
2022-08-08 22:31:29 +02:00
new_lbry_id_password , new_hmac_key = derive_secrets (
2022-07-15 21:36:11 +02:00
new_root_password , self . email , self . salt_seed )
2022-08-08 22:31:29 +02:00
def set_secrets ( ) :
# Only do this once we got a good response from the server.
# In a function because it can happen in two different places.
self . root_password , self . lbry_id_password , self . hmac_key = (
new_root_password , new_lbry_id_password , new_hmac_key )
2022-07-15 21:36:11 +02:00
# TODO - Think of failure sequence in case of who knows what. We
# could just get the old salt seed back from the server?
# We can't lose it though. Keep the old one around? Kinda sucks.
2022-07-06 23:55:15 +02:00
if self . synced_wallet_state and self . synced_wallet_state . sequence > 0 :
2022-07-08 18:36:10 +02:00
# Don't allow it to change if we have local changes to push. This
# precludes the possibility of having a conflict with remote changes,
# followed by a merge with user interaction, when the user is already in
# the middle of a password change. This way, if there is a conflict, we
# can simply get the latest wallet and try again with the same password
# that the user just entered, guaranteeing that they won't need to do any
# more interactions.
#
# NOTE: If for whatever reason this is removed, make sure to add a call
# to mark_local_changes_synced as appropriate below, since we may be
# going from unsynced to synced.
if self . has_unsynced_local_changes ( ) :
print ( " Local changes found. Update remote wallet before changing password. " )
return " Failure "
2022-07-06 23:55:15 +02:00
# 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 (
2022-08-08 22:31:29 +02:00
encrypted_wallet = self . get_local_encrypted_wallet ( new_root_password ) ,
2022-07-06 23:55:15 +02:00
sequence = self . synced_wallet_state . sequence + 1
)
hmac = create_hmac ( submitted_wallet_state , new_hmac_key )
# Update our password and submit our wallet.
2022-07-15 21:36:11 +02:00
updated = self . wallet_sync_api . change_password_with_wallet ( submitted_wallet_state , hmac , self . email , self . lbry_id_password , new_lbry_id_password , self . salt_seed )
2022-07-06 23:55:15 +02:00
if updated :
2022-08-08 22:31:29 +02:00
# We updated it. Now it's synced and we mark it as such. Update everything at once to keep local changes in sync!
self . synced_wallet_state = submitted_wallet_state
set_secrets ( )
2022-07-06 23:55:15 +02:00
print ( " Synced walletState: " )
pprint ( self . synced_wallet_state )
return " Success "
else :
# Update our password.
2022-07-15 21:36:11 +02:00
updated = self . wallet_sync_api . change_password_no_wallet ( self . email , self . lbry_id_password , new_lbry_id_password , self . salt_seed )
2022-07-06 23:55:15 +02:00
if updated :
2022-08-08 22:31:29 +02:00
# We updated it. Now we mark it as such. Update everything at once to keep local changes in sync!
set_secrets ( )
2022-07-06 23:55:15 +02:00
return " Success "
2022-07-07 18:33:18 +02:00
print ( " Could not update wallet and password. Perhaps need to get new wallet and merge, perhaps something else. " )
2022-07-06 23:55:15 +02:00
return " Failure "
2022-06-14 23:26:57 +02:00
def set_preference ( self , key , value ) :
2022-06-16 04:21:00 +02:00
return LBRYSDK . set_preference ( self . wallet_id , key , value )
2021-12-25 02:16:58 +01:00
2022-06-14 23:26:57 +02:00
def get_preferences ( self ) :
2022-06-16 04:21:00 +02:00
return LBRYSDK . get_preferences ( self . wallet_id )
2021-12-25 02:16:58 +01:00
2022-07-08 18:36:10 +02:00
def has_unsynced_local_changes ( self ) :
return self . lbry_sdk_last_synced_hash != LBRYSDK . get_hash ( self . wallet_id )
def mark_local_changes_synced ( self ) :
self . lbry_sdk_last_synced_hash = LBRYSDK . get_hash ( self . wallet_id )
def mark_local_changes_synced_to_empty ( self ) :
# TODO - this should be the hash of the empty wallet. See
# comment in init_wallet_state().
self . lbry_sdk_last_synced_hash = " "
2022-06-14 23:26:57 +02:00
def update_local_encrypted_wallet ( self , encrypted_wallet ) :
2022-08-08 22:31:29 +02:00
return LBRYSDK . update_wallet ( self . wallet_id , self . root_password , encrypted_wallet )
2021-12-25 02:16:58 +01:00
2022-07-06 23:55:15 +02:00
def get_local_encrypted_wallet ( self , sync_password ) :
2022-08-09 18:01:56 +02:00
# Note for auditor: sync_password here is now the root_password. The SDK
# has its own KDF (though with different Scrypt parameters as of this
# writing). So in all:
# root password -> APP KDF -> (HMAC, wallet sync server password)
# root password -> SDK KDF -> (wallet encryption for remote storage, wallet "locking" (encryption) for local storage)
# The App uses the Salt Seed system from Standard Notes, the SDK creates a
# random salt every encryption. So (for now) we're not sharing salts
# between the KDFs. The question is, is it safe to use the same root
# password on two two different KDFs like this?
2022-07-06 23:55:15 +02:00
return LBRYSDK . get_wallet ( self . wallet_id , sync_password )