Proper KDF, though I need to tweak parameters

This commit is contained in:
Daniel Krol 2022-06-16 17:06:55 -04:00
parent 5d14041c86
commit e2a4e18a43

View file

@ -1,7 +1,9 @@
#!/bin/python3 #!/bin/python3
from collections import namedtuple from collections import namedtuple
import json, uuid, requests, hashlib import base64, json, uuid, requests, hashlib
from pprint import pprint from pprint import pprint
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.backends import default_backend as crypto_default_backend
WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet']) WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet'])
@ -150,22 +152,47 @@ class WalletSync():
hmac = response.json()['hmac'] hmac = response.json()['hmac']
return wallet_state, hmac, conflict return wallet_state, hmac, conflict
# TODO - do this correctly. This is a hack example. def derive_secrets(root_password, salt):
def derive_login_password(root_password): # TODO - Audit me audit me audit me! I don't know if these values are
return hashlib.sha256(('login:' + root_password).encode('utf-8')).hexdigest() # optimal.
#
# I will say that it seems like there's an optimal for access control, and
# there's a stronger optimal for sensitive storage.
# TODO - wallet_id in the salt? (with domain etc if we go that way)
# But, we probably want random salt anyway for each domain, who cares
scrypt_n = 1<<13
scrypt_r = 16
scrypt_p = 1
# TODO - do this correctly. This is a hack example. key_length = 32
def derive_sdk_password(root_password): num_keys = 3
return hashlib.sha256(('sdk:' + root_password).encode('utf-8')).hexdigest()
# TODO - do this correctly. This is a hack example. kdf = Scrypt(
# TODO - wallet_id in this? or in all of the derivations? salt,
def derive_hmac_key(root_password): length=key_length * num_keys,
return hashlib.sha256(('hmac:' + root_password).encode('utf-8')).hexdigest() n=scrypt_n,
r=scrypt_r,
p=scrypt_p,
backend=crypto_default_backend(),
)
kdf_output = kdf.derive(root_password)
# Split the output in three
parts = (
kdf_output[:key_length],
kdf_output[key_length:key_length * 2],
kdf_output[key_length * 2:],
)
wallet_sync_password = base64.b64encode(parts[0]).decode('utf-8')
sdk_password = base64.b64encode(parts[1]).decode('utf-8')
hmac_key = parts[2]
return wallet_sync_password, sdk_password, hmac_key
# TODO - do this correctly. This is a hack example. # TODO - do this correctly. This is a hack example.
def create_hmac(wallet_state, hmac_key): def create_hmac(wallet_state, hmac_key):
input_str = hmac_key + ':' + str(wallet_state.sequence) + ':' + wallet_state.encrypted_wallet input_str = base64.b64encode(hmac_key).decode('utf-8') + ':' + str(wallet_state.sequence) + ':' + wallet_state.encrypted_wallet
return hashlib.sha256(input_str.encode('utf-8')).hexdigest() return hashlib.sha256(input_str.encode('utf-8')).hexdigest()
def check_hmac(wallet_state, hmac_key, hmac): def check_hmac(wallet_state, hmac_key, hmac):
@ -192,13 +219,20 @@ class Client():
def __init__(self, email, root_password, wallet_id='default_wallet', local=False): def __init__(self, email, root_password, wallet_id='default_wallet', local=False):
# Represents normal client behavior (though a real client will of course save device id) # Represents normal client behavior (though a real client will of course save device id)
self.device_id = str(uuid.uuid4()) self.device_id = str(uuid.uuid4())
self.auth_token = 'bad token' self.auth_token = 'bad token'
self.synced_wallet_state = None self.synced_wallet_state = None
self.email = email self.email = email
self.root_password = root_password
# TODO - generate randomly CLIENT SIDE and post to server with
# registration. And maybe get it to new clients along with the auth token.
# But is there an attack vector if we don't trust the salt? See how others
# do it. Since the same server sees one of the outputs of the KDF. Huh.
self.salt = b'I AM A SALT'
# TODO - is UTF-8 appropriate for root_password? based on characters used etc.
self.wallet_sync_password, self.sdk_password, self.hmac_key = derive_secrets(bytes(root_password, 'utf-8'), self.salt)
self.wallet_id = wallet_id self.wallet_id = wallet_id
self.wallet_sync_api = WalletSync(local=local) self.wallet_sync_api = WalletSync(local=local)
@ -248,7 +282,7 @@ class Client():
def register(self): def register(self):
success = self.wallet_sync_api.register( success = self.wallet_sync_api.register(
self.email, self.email,
derive_login_password(self.root_password), self.wallet_sync_password,
) )
if success: if success:
print ("Registered") print ("Registered")
@ -256,7 +290,7 @@ class Client():
def get_auth_token(self): def get_auth_token(self):
token = self.wallet_sync_api.get_auth_token( token = self.wallet_sync_api.get_auth_token(
self.email, self.email,
derive_login_password(self.root_password), self.wallet_sync_password,
self.device_id, self.device_id,
) )
if not token: if not token:
@ -279,8 +313,7 @@ class Client():
# Wallet not found, but this is not an error # Wallet not found, but this is not an error
return "Not Found" return "Not Found"
hmac_key = derive_hmac_key(self.root_password) if not check_hmac(new_wallet_state, self.hmac_key, hmac):
if not check_hmac(new_wallet_state, hmac_key, hmac):
print ('Error - bad hmac on new wallet') print ('Error - bad hmac on new wallet')
print (new_wallet_state, hmac) print (new_wallet_state, hmac)
return "Error" return "Error"
@ -309,20 +342,18 @@ class Client():
print ("No wallet state to post.") print ("No wallet state to post.")
return "Error" return "Error"
hmac_key = derive_hmac_key(self.root_password)
submitted_wallet_state = WalletState( submitted_wallet_state = WalletState(
encrypted_wallet=self.get_local_encrypted_wallet(), encrypted_wallet=self.get_local_encrypted_wallet(),
sequence=self.synced_wallet_state.sequence + 1 sequence=self.synced_wallet_state.sequence + 1
) )
hmac = create_hmac(submitted_wallet_state, hmac_key) hmac = create_hmac(submitted_wallet_state, self.hmac_key)
# Submit our wallet, get the latest wallet back as a response # Submit our wallet, get the latest wallet back as a response
new_wallet_state, new_hmac, conflict = self.wallet_sync_api.update_wallet(submitted_wallet_state, hmac, self.auth_token) new_wallet_state, new_hmac, conflict = self.wallet_sync_api.update_wallet(submitted_wallet_state, hmac, self.auth_token)
# TODO - there's some code in common here with the get_remote_wallet function. factor it out. # TODO - there's some code in common here with the get_remote_wallet function. factor it out.
if not check_hmac(new_wallet_state, hmac_key, new_hmac): if not check_hmac(new_wallet_state, self.hmac_key, new_hmac):
print ('Error - bad hmac on new wallet') print ('Error - bad hmac on new wallet')
print (new_wallet_state, hmac) print (new_wallet_state, hmac)
return "Error" return "Error"
@ -353,8 +384,8 @@ class Client():
def update_local_encrypted_wallet(self, encrypted_wallet): def update_local_encrypted_wallet(self, encrypted_wallet):
# TODO - error checking # TODO - error checking
return LBRYSDK.update_wallet(self.wallet_id, derive_sdk_password(self.root_password), encrypted_wallet) return LBRYSDK.update_wallet(self.wallet_id, self.sdk_password, encrypted_wallet)
def get_local_encrypted_wallet(self): def get_local_encrypted_wallet(self):
# TODO - error checking # TODO - error checking
return LBRYSDK.get_wallet(self.wallet_id, derive_sdk_password(self.root_password)) return LBRYSDK.get_wallet(self.wallet_id, self.sdk_password)