allow encrypting watch-only wallets. initial support for hw wallet storage encryption.

This commit is contained in:
SomberNight 2017-12-07 11:35:10 +01:00 committed by SomberNight
parent 743ef9ec8f
commit c811c5c9d9
20 changed files with 507 additions and 146 deletions

View File

@ -192,6 +192,8 @@ def init_daemon(config_options):
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
sys.exit(0)
if storage.is_encrypted():
if storage.is_encrypted_with_hw_device():
raise NotImplementedError("CLI functionality of encrypted hw wallets")
if config.get('password'):
password = config.get('password')
else:
@ -236,6 +238,8 @@ def init_cmdline(config_options, server):
# commands needing password
if (cmd.requires_wallet and storage.is_encrypted() and server is None)\
or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())):
if storage.is_encrypted_with_hw_device():
raise NotImplementedError("CLI functionality of encrypted hw wallets")
if config.get('password'):
password = config.get('password')
else:
@ -262,12 +266,14 @@ def run_offline_command(config, config_options):
if cmd.requires_wallet:
storage = WalletStorage(config.get_wallet_path())
if storage.is_encrypted():
if storage.is_encrypted_with_hw_device():
raise NotImplementedError("CLI functionality of encrypted hw wallets")
storage.decrypt(password)
wallet = Wallet(storage)
else:
wallet = None
# check password
if cmd.requires_password and storage.get('use_encryption'):
if cmd.requires_password and wallet.has_password():
try:
seed = wallet.check_password(password)
except InvalidPassword:

View File

@ -807,7 +807,7 @@ class InstallWizard(BaseWizard, Widget):
popup.init(message, callback)
popup.open()
def request_password(self, run_next):
def request_password(self, run_next, force_disable_encrypt_cb=False):
def callback(pin):
if pin:
self.run('confirm_password', pin, run_next)

View File

@ -10,13 +10,13 @@ from PyQt5.QtWidgets import *
from electrum import Wallet, WalletStorage
from electrum.util import UserCancelled, InvalidPassword
from electrum.base_wizard import BaseWizard
from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET
from electrum.i18n import _
from .seed_dialog import SeedLayout, KeysLayout
from .network_dialog import NetworkChoiceLayout
from .util import *
from .password_dialog import PasswordLayout, PW_NEW
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
class GoBack(Exception):
@ -29,6 +29,10 @@ MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or x
MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
+ _("Leave this field empty if you want to disable encryption.")
MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
+ _("Your wallet file does not contain secrets, mostly just metadata. ") \
+ _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
+ _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
MSG_RESTORE_PASSPHRASE = \
_("Please enter your seed derivation passphrase. "
"Note: this is NOT your encryption password. "
@ -196,12 +200,18 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
msg =_("This file does not exist.") + '\n' \
+ _("Press 'Next' to create this wallet, or choose another file.")
pw = False
elif self.storage.file_exists() and self.storage.is_encrypted():
msg = _("This file is encrypted.") + '\n' + _('Enter your password or choose another file.')
pw = True
else:
msg = _("Press 'Next' to open this wallet.")
pw = False
if self.storage.is_encrypted_with_user_pw():
msg = _("This file is encrypted with a password.") + '\n' \
+ _('Enter your password or choose another file.')
pw = True
elif self.storage.is_encrypted_with_hw_device():
msg = _("This file is encrypted using a hardware device.") + '\n' \
+ _("Press 'Next' to choose device to decrypt.")
pw = False
else:
msg = _("Press 'Next' to open this wallet.")
pw = False
else:
msg = _('Cannot read file')
pw = False
@ -227,17 +237,40 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
if not self.storage.file_exists():
break
if self.storage.file_exists() and self.storage.is_encrypted():
password = self.pw_e.text()
try:
self.storage.decrypt(password)
break
except InvalidPassword as e:
QMessageBox.information(None, _('Error'), str(e))
continue
except BaseException as e:
traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e))
return
if self.storage.is_encrypted_with_user_pw():
password = self.pw_e.text()
try:
self.storage.decrypt(password)
break
except InvalidPassword as e:
QMessageBox.information(None, _('Error'), str(e))
continue
except BaseException as e:
traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e))
return
elif self.storage.is_encrypted_with_hw_device():
try:
self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET)
except InvalidPassword as e:
# FIXME if we get here because of mistyped passphrase
# then that passphrase gets "cached"
QMessageBox.information(
None, _('Error'),
_('Failed to decrypt using this hardware device.') + '\n' +
_('If you use a passphrase, make sure it is correct.'))
self.stack = []
return self.run_and_get_wallet()
except BaseException as e:
traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e))
return
if self.storage.is_past_initial_decryption():
break
else:
return
else:
raise Exception('Unexpected encryption version')
path = self.storage.path
if self.storage.requires_split():
@ -386,17 +419,25 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.exec_layout(slayout)
return slayout.is_ext
def pw_layout(self, msg, kind):
playout = PasswordLayout(None, msg, kind, self.next_button)
def pw_layout(self, msg, kind, force_disable_encrypt_cb):
playout = PasswordLayout(None, msg, kind, self.next_button,
force_disable_encrypt_cb=force_disable_encrypt_cb)
playout.encrypt_cb.setChecked(True)
self.exec_layout(playout.layout())
return playout.new_password(), playout.encrypt_cb.isChecked()
@wizard_dialog
def request_password(self, run_next):
def request_password(self, run_next, force_disable_encrypt_cb=False):
"""Request the user enter a new password and confirm it. Return
the password or None for no password."""
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW)
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb)
@wizard_dialog
def request_storage_encryption(self, run_next):
playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button)
playout.encrypt_cb.setChecked(True)
self.exec_layout(playout.layout())
return playout.encrypt_cb.isChecked()
def show_restore(self, wallet, network):
# FIXME: these messages are shown after the install wizard is

View File

@ -372,7 +372,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
extra.append(_('watching only'))
title += ' [%s]'% ', '.join(extra)
self.setWindowTitle(title)
self.password_menu.setEnabled(self.wallet.can_change_password())
self.password_menu.setEnabled(self.wallet.may_have_password())
self.import_privkey_menu.setVisible(self.wallet.can_import_privkey())
self.import_address_menu.setVisible(self.wallet.can_import_address())
self.export_menu.setEnabled(self.wallet.can_export())
@ -877,14 +877,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if alias_addr:
if self.wallet.is_mine(alias_addr):
msg = _('This payment request will be signed.') + '\n' + _('Please enter your password')
password = self.password_dialog(msg)
if password:
try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e:
self.show_error(str(e))
password = None
if self.wallet.has_keystore_encryption():
password = self.password_dialog(msg)
if not password:
return
else:
try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e:
self.show_error(str(e))
return
else:
return
@ -1372,7 +1373,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def request_password(self, *args, **kwargs):
parent = self.top_level_window()
password = None
while self.wallet.has_password():
while self.wallet.has_keystore_encryption():
password = self.password_dialog(parent=parent)
if password is None:
# User cancelled password input
@ -1506,7 +1507,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if fee > confirm_rate * tx.estimated_size() / 1000:
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
if self.wallet.has_password():
if self.wallet.has_keystore_encryption():
msg.append("")
msg.append(_("Enter your password to proceed"))
password = self.password_dialog('\n'.join(msg))
@ -1909,17 +1910,37 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def update_buttons_on_seed(self):
self.seed_button.setVisible(self.wallet.has_seed())
self.password_button.setVisible(self.wallet.can_change_password())
self.password_button.setVisible(self.wallet.may_have_password())
self.send_button.setVisible(not self.wallet.is_watching_only())
def change_password_dialog(self):
from .password_dialog import ChangePasswordDialog
d = ChangePasswordDialog(self, self.wallet)
ok, password, new_password, encrypt_file = d.run()
from electrum.storage import STO_EV_XPUB_PW
if self.wallet.get_available_storage_encryption_version() == STO_EV_XPUB_PW:
from .password_dialog import ChangePasswordDialogForHW
d = ChangePasswordDialogForHW(self, self.wallet)
ok, encrypt_file = d.run()
if not ok:
return
try:
hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption()
except UserCancelled:
return
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.show_error(str(e))
return
old_password = hw_dev_pw if self.wallet.has_password() else None
new_password = hw_dev_pw if encrypt_file else None
else:
from .password_dialog import ChangePasswordDialogForSW
d = ChangePasswordDialogForSW(self, self.wallet)
ok, old_password, new_password, encrypt_file = d.run()
if not ok:
return
try:
self.wallet.update_password(password, new_password, encrypt_file)
self.wallet.update_password(old_password, new_password, encrypt_file)
except BaseException as e:
self.show_error(str(e))
return
@ -1927,7 +1948,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
traceback.print_exc(file=sys.stdout)
self.show_error(_('Failed to update password'))
return
msg = _('Password was updated successfully') if new_password else _('Password is disabled, this wallet is not protected')
msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected')
self.show_message(msg, title=_("Success"))
self.update_lock_icon()

View File

@ -57,7 +57,7 @@ class PasswordLayout(object):
titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")]
def __init__(self, wallet, msg, kind, OK_button):
def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False):
self.wallet = wallet
self.pw = QLineEdit()
@ -126,7 +126,8 @@ class PasswordLayout(object):
def enable_OK():
ok = self.new_pw.text() == self.conf_pw.text()
OK_button.setEnabled(ok)
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()))
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())
and not force_disable_encrypt_cb)
self.new_pw.textChanged.connect(enable_OK)
self.conf_pw.textChanged.connect(enable_OK)
@ -163,11 +164,84 @@ class PasswordLayout(object):
return pw
class ChangePasswordDialog(WindowModalDialog):
class PasswordLayoutForHW(object):
def __init__(self, wallet, msg, kind, OK_button):
self.wallet = wallet
self.kind = kind
self.OK_button = OK_button
vbox = QVBoxLayout()
label = QLabel(msg + "\n")
label.setWordWrap(True)
grid = QGridLayout()
grid.setSpacing(8)
grid.setColumnMinimumWidth(0, 150)
grid.setColumnMinimumWidth(1, 100)
grid.setColumnStretch(1,1)
logo_grid = QGridLayout()
logo_grid.setSpacing(8)
logo_grid.setColumnMinimumWidth(0, 70)
logo_grid.setColumnStretch(1,1)
logo = QLabel()
logo.setAlignment(Qt.AlignCenter)
logo_grid.addWidget(logo, 0, 0)
logo_grid.addWidget(label, 0, 1, 1, 2)
vbox.addLayout(logo_grid)
if wallet and wallet.has_storage_encryption():
lockfile = ":icons/lock.png"
else:
lockfile = ":icons/unlock.png"
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
vbox.addLayout(grid)
self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
grid.addWidget(self.encrypt_cb, 1, 0, 1, 2)
self.vbox = vbox
def title(self):
return _("Toggle Encryption")
def layout(self):
return self.vbox
class ChangePasswordDialogBase(WindowModalDialog):
def __init__(self, parent, wallet):
WindowModalDialog.__init__(self, parent)
is_encrypted = wallet.storage.is_encrypted()
is_encrypted = wallet.has_storage_encryption()
OK_button = OkButton(self)
self.create_password_layout(wallet, is_encrypted, OK_button)
self.setWindowTitle(self.playout.title())
vbox = QVBoxLayout(self)
vbox.addLayout(self.playout.layout())
vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self), OK_button))
self.playout.encrypt_cb.setChecked(is_encrypted)
def create_password_layout(self, wallet, is_encrypted, OK_button):
raise NotImplementedError()
class ChangePasswordDialogForSW(ChangePasswordDialogBase):
def __init__(self, parent, wallet):
ChangePasswordDialogBase.__init__(self, parent, wallet)
if not wallet.has_password():
self.playout.encrypt_cb.setChecked(True)
def create_password_layout(self, wallet, is_encrypted, OK_button):
if not wallet.has_password():
msg = _('Your wallet is not protected.')
msg += ' ' + _('Use this dialog to add a password to your wallet.')
@ -177,14 +251,9 @@ class ChangePasswordDialog(WindowModalDialog):
else:
msg = _('Your wallet is password protected and encrypted.')
msg += ' ' + _('Use this dialog to change your password.')
OK_button = OkButton(self)
self.playout = PasswordLayout(wallet, msg, PW_CHANGE, OK_button)
self.setWindowTitle(self.playout.title())
vbox = QVBoxLayout(self)
vbox.addLayout(self.playout.layout())
vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self), OK_button))
self.playout.encrypt_cb.setChecked(is_encrypted or not wallet.has_password())
self.playout = PasswordLayout(
wallet, msg, PW_CHANGE, OK_button,
force_disable_encrypt_cb=not wallet.can_have_keystore_encryption())
def run(self):
if not self.exec_():
@ -192,6 +261,26 @@ class ChangePasswordDialog(WindowModalDialog):
return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
class ChangePasswordDialogForHW(ChangePasswordDialogBase):
def __init__(self, parent, wallet):
ChangePasswordDialogBase.__init__(self, parent, wallet)
def create_password_layout(self, wallet, is_encrypted, OK_button):
if not is_encrypted:
msg = _('Your wallet file is NOT encrypted.')
else:
msg = _('Your wallet file is encrypted.')
msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.')
msg += '\n' + _('Use this dialog to toggle encryption.')
self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button)
def run(self):
if not self.exec_():
return False, None
return True, self.playout.encrypt_cb.isChecked()
class PasswordDialog(WindowModalDialog):
def __init__(self, parent=None, msg=None):

View File

@ -24,12 +24,19 @@
# SOFTWARE.
import os
import sys
import traceback
from . import bitcoin
from . import keystore
from .keystore import bip44_derivation
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
from .i18n import _
from .util import UserCancelled
# hardware device setup purpose
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
class ScriptTypeNotSupported(Exception): pass
@ -147,17 +154,22 @@ class BaseWizard(object):
is_valid=v, allow_multi=True)
def on_import(self, text):
# create a temporary wallet and exploit that modifications
# will be reflected on self.storage
if keystore.is_address_list(text):
self.wallet = Imported_Wallet(self.storage)
w = Imported_Wallet(self.storage)
for x in text.split():
self.wallet.import_address(x)
w.import_address(x)
elif keystore.is_private_key_list(text):
k = keystore.Imported_KeyStore({})
self.storage.put('keystore', k.dump())
self.wallet = Imported_Wallet(self.storage)
w = Imported_Wallet(self.storage)
for x in text.split():
self.wallet.import_private_key(x, None)
self.terminate()
w.import_private_key(x, None)
self.keystores.append(w.keystore)
else:
return self.terminate()
return self.run('create_wallet')
def restore_from_key(self):
if self.wallet_type == 'standard':
@ -176,7 +188,7 @@ class BaseWizard(object):
k = keystore.from_master_key(text)
self.on_keystore(k)
def choose_hw_device(self):
def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET):
title = _('Hardware Keystore')
# check available plugins
support = self.plugins.get_hardware_support()
@ -185,7 +197,7 @@ class BaseWizard(object):
_('No hardware wallet support found on your system.'),
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
])
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
return
# scan devices
devices = []
@ -205,7 +217,7 @@ class BaseWizard(object):
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ',
_('On Linux, you might have to add a new permission to your udev rules.'),
])
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
return
# select device
self.devices = devices
@ -216,23 +228,31 @@ class BaseWizard(object):
descr = "%s [%s, %s]" % (label, name, state)
choices.append(((name, info), descr))
msg = _('Select a device') + ':'
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_device)
self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose))
def on_device(self, name, device_info):
def on_device(self, name, device_info, *, purpose):
self.plugin = self.plugins.get_plugin(name)
try:
self.plugin.setup_device(device_info, self)
self.plugin.setup_device(device_info, self, purpose)
except BaseException as e:
self.show_error(str(e))
self.choose_hw_device()
self.choose_hw_device(purpose)
return
if self.wallet_type=='multisig':
# There is no general standard for HD multisig.
# This is partially compatible with BIP45; assumes index=0
self.on_hw_derivation(name, device_info, "m/45'/0")
if purpose == HWD_SETUP_NEW_WALLET:
if self.wallet_type=='multisig':
# There is no general standard for HD multisig.
# This is partially compatible with BIP45; assumes index=0
self.on_hw_derivation(name, device_info, "m/45'/0")
else:
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
self.derivation_dialog(f)
elif purpose == HWD_SETUP_DECRYPT_WALLET:
derivation = get_derivation_used_for_hw_device_encryption()
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self)
password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
self.storage.decrypt(password)
else:
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
self.derivation_dialog(f)
raise Exception('unknown purpose: %s' % purpose)
def derivation_dialog(self, f):
default = bip44_derivation(0, bip43_purpose=44)
@ -365,13 +385,45 @@ class BaseWizard(object):
self.run('create_wallet')
def create_wallet(self):
if any(k.may_have_password() for k in self.keystores):
self.request_password(run_next=self.on_password)
encrypt_keystore = any(k.may_have_password() for k in self.keystores)
# note: the following condition ("if") is duplicated logic from
# wallet.get_available_storage_encryption_version()
if self.wallet_type == 'standard' and isinstance(self.keystores[0], keystore.Hardware_KeyStore):
# offer encrypting with a pw derived from the hw device
k = self.keystores[0]
try:
k.handler = self.plugin.create_handler(self)
password = k.get_password_for_storage_encryption()
except UserCancelled:
devmgr = self.plugins.device_manager
devmgr.unpair_xpub(k.xpub)
self.choose_hw_device()
return
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.show_error(str(e))
return
self.request_storage_encryption(
run_next=lambda encrypt_storage: self.on_password(
password,
encrypt_storage=encrypt_storage,
storage_enc_version=STO_EV_XPUB_PW,
encrypt_keystore=False))
else:
self.on_password(None, False)
# prompt the user to set an arbitrary password
self.request_password(
run_next=lambda password, encrypt_storage: self.on_password(
password,
encrypt_storage=encrypt_storage,
storage_enc_version=STO_EV_USER_PW,
encrypt_keystore=encrypt_keystore),
force_disable_encrypt_cb=not encrypt_keystore)
def on_password(self, password, encrypt):
self.storage.set_password(password, encrypt)
def on_password(self, password, *, encrypt_storage,
storage_enc_version=STO_EV_USER_PW, encrypt_keystore):
self.storage.set_keystore_encryption(bool(password) and encrypt_keystore)
if encrypt_storage:
self.storage.set_password(password, enc_version=storage_enc_version)
for k in self.keystores:
if k.may_have_password():
k.update_password(None, password)
@ -387,6 +439,13 @@ class BaseWizard(object):
self.storage.write()
self.wallet = Multisig_Wallet(self.storage)
self.run('create_addresses')
elif self.wallet_type == 'imported':
if len(self.keystores) > 0:
keys = self.keystores[0].dump()
self.storage.put('keystore', keys)
self.wallet = Imported_Wallet(self.storage)
self.wallet.storage.write()
self.terminate()
def show_xpub_and_add_cosigners(self, xpub):
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))

View File

@ -643,8 +643,8 @@ def verify_message(address, sig, message):
return False
def encrypt_message(message, pubkey):
return EC_KEY.encrypt_message(message, bfh(pubkey))
def encrypt_message(message, pubkey, magic=b'BIE1'):
return EC_KEY.encrypt_message(message, bfh(pubkey), magic)
def chunks(l, n):
@ -789,7 +789,7 @@ class EC_KEY(object):
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
@classmethod
def encrypt_message(self, message, pubkey):
def encrypt_message(self, message, pubkey, magic=b'BIE1'):
assert_bytes(message)
pk = ser_to_point(pubkey)
@ -803,20 +803,20 @@ class EC_KEY(object):
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True))
encrypted = b'BIE1' + ephemeral_pubkey + ciphertext
encrypted = magic + ephemeral_pubkey + ciphertext
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
return base64.b64encode(encrypted + mac)
def decrypt_message(self, encrypted):
def decrypt_message(self, encrypted, magic=b'BIE1'):
encrypted = base64.b64decode(encrypted)
if len(encrypted) < 85:
raise Exception('invalid ciphertext: length')
magic = encrypted[:4]
magic_found = encrypted[:4]
ephemeral_pubkey = encrypted[4:37]
ciphertext = encrypted[37:-32]
mac = encrypted[-32:]
if magic != b'BIE1':
if magic_found != magic:
raise Exception('invalid ciphertext: invalid magic bytes')
try:
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)

View File

@ -82,7 +82,7 @@ def command(s):
password = kwargs.get('password')
if c.requires_wallet and wallet is None:
raise BaseException("wallet not loaded. Use 'electrum daemon load_wallet'")
if c.requires_password and password is None and wallet.storage.get('use_encryption'):
if c.requires_password and password is None and wallet.has_password():
return {'error': 'Password required' }
return func(*args, **kwargs)
return func_wrapper

View File

@ -45,6 +45,10 @@ class KeyStore(PrintError):
def can_import(self):
return False
def may_have_password(self):
"""Returns whether the keystore can be encrypted with a password."""
raise NotImplementedError()
def get_tx_derivations(self, tx):
keypairs = {}
for txin in tx.inputs():
@ -116,9 +120,6 @@ class Imported_KeyStore(Software_KeyStore):
def is_deterministic(self):
return False
def can_change_password(self):
return True
def get_master_public_key(self):
return None
@ -196,9 +197,6 @@ class Deterministic_KeyStore(Software_KeyStore):
def is_watching_only(self):
return not self.has_seed()
def can_change_password(self):
return not self.is_watching_only()
def add_seed(self, seed):
if self.seed:
raise Exception("a seed exists")
@ -522,9 +520,13 @@ class Hardware_KeyStore(KeyStore, Xpub):
assert not self.has_seed()
return False
def can_change_password(self):
return False
def get_password_for_storage_encryption(self):
from .storage import get_derivation_used_for_hw_device_encryption
client = self.plugin.get_client(self)
derivation = get_derivation_used_for_hw_device_encryption()
xpub = client.get_xpub(derivation, "standard")
password = self.get_pubkey_from_xpub(xpub, ())
return password
def bip39_normalize_passphrase(passphrase):

View File

@ -33,7 +33,7 @@ import pbkdf2, hmac, hashlib
import base64
import zlib
from .util import PrintError, profiler
from .util import PrintError, profiler, InvalidPassword
from .plugins import run_hook, plugin_loaders
from .keystore import bip44_derivation
from . import bitcoin
@ -56,6 +56,13 @@ def multisig_type(wallet_type):
match = [int(x) for x in match.group(1, 2)]
return match
def get_derivation_used_for_hw_device_encryption():
return ("m"
"/4541509'" # ascii 'ELE' as decimal ("BIP43 purpose")
"/1112098098'") # ascii 'BIE2' as decimal
# storage encryption version
STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3)
class WalletStorage(PrintError):
@ -70,9 +77,11 @@ class WalletStorage(PrintError):
if self.file_exists():
with open(self.path, "r") as f:
self.raw = f.read()
self._encryption_version = self._init_encryption_version()
if not self.is_encrypted():
self.load_data(self.raw)
else:
self._encryption_version = STO_EV_PLAINTEXT
# avoid new wallets getting 'upgraded'
self.put('seed_version', FINAL_SEED_VERSION)
@ -106,11 +115,47 @@ class WalletStorage(PrintError):
if self.requires_upgrade():
self.upgrade()
def is_past_initial_decryption(self):
"""Return if storage is in a usable state for normal operations.
The value is True exactly
if encryption is disabled completely (self.is_encrypted() == False),
or if encryption is enabled but the contents have already been decrypted.
"""
return bool(self.data)
def is_encrypted(self):
"""Return if storage encryption is currently enabled."""
return self.get_encryption_version() != STO_EV_PLAINTEXT
def is_encrypted_with_user_pw(self):
return self.get_encryption_version() == STO_EV_USER_PW
def is_encrypted_with_hw_device(self):
return self.get_encryption_version() == STO_EV_XPUB_PW
def get_encryption_version(self):
"""Return the version of encryption used for this storage.
0: plaintext / no encryption
ECIES, private key derived from a password,
1: password is provided by user
2: password is derived from an xpub; used with hw wallets
"""
return self._encryption_version
def _init_encryption_version(self):
try:
return base64.b64decode(self.raw)[0:4] == b'BIE1'
magic = base64.b64decode(self.raw)[0:4]
if magic == b'BIE1':
return STO_EV_USER_PW
elif magic == b'BIE2':
return STO_EV_XPUB_PW
else:
return STO_EV_PLAINTEXT
except:
return False
return STO_EV_PLAINTEXT
def file_exists(self):
return self.path and os.path.exists(self.path)
@ -120,20 +165,50 @@ class WalletStorage(PrintError):
ec_key = bitcoin.EC_KEY(secret)
return ec_key
def _get_encryption_magic(self):
v = self._encryption_version
if v == STO_EV_USER_PW:
return b'BIE1'
elif v == STO_EV_XPUB_PW:
return b'BIE2'
else:
raise Exception('no encryption magic for version: %s' % v)
def decrypt(self, password):
ec_key = self.get_key(password)
s = zlib.decompress(ec_key.decrypt_message(self.raw)) if self.raw else None
if self.raw:
enc_magic = self._get_encryption_magic()
s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic))
else:
s = None
self.pubkey = ec_key.get_public_key()
s = s.decode('utf8')
self.load_data(s)
def set_password(self, password, encrypt):
self.put('use_encryption', bool(password))
if encrypt and password:
def check_password(self, password):
"""Raises an InvalidPassword exception on invalid password"""
if not self.is_encrypted():
return
if self.pubkey and self.pubkey != self.get_key(password).get_public_key():
raise InvalidPassword()
def set_keystore_encryption(self, enable):
self.put('use_encryption', enable)
def set_password(self, password, enc_version=None):
"""Set a password to be used for encrypting this storage."""
if enc_version is None:
enc_version = self._encryption_version
if password and enc_version != STO_EV_PLAINTEXT:
ec_key = self.get_key(password)
self.pubkey = ec_key.get_public_key()
self._encryption_version = enc_version
else:
self.pubkey = None
self._encryption_version = STO_EV_PLAINTEXT
# make sure next storage.write() saves changes
with self.lock:
self.modified = True
def get(self, key, default=None):
with self.lock:
@ -175,7 +250,8 @@ class WalletStorage(PrintError):
if self.pubkey:
s = bytes(s, 'utf8')
c = zlib.compress(s)
s = bitcoin.encrypt_message(c, self.pubkey)
enc_magic = self._get_encryption_magic()
s = bitcoin.encrypt_message(c, self.pubkey, enc_magic)
s = s.decode('utf8')
temp_path = "%s.tmp.%s" % (self.path, os.getpid())

View File

@ -48,7 +48,7 @@ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
from .bitcoin import *
from .version import *
from .keystore import load_keystore, Hardware_KeyStore
from .storage import multisig_type
from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW
from . import transaction
from .transaction import Transaction
@ -1359,10 +1359,65 @@ class Abstract_Wallet(PrintError):
self.synchronizer.add(address)
def has_password(self):
return self.storage.get('use_encryption', False)
return self.has_keystore_encryption() or self.has_storage_encryption()
def can_have_keystore_encryption(self):
return self.keystore and self.keystore.may_have_password()
def get_available_storage_encryption_version(self):
"""Returns the type of storage encryption offered to the user.
A wallet file (storage) is either encrypted with this version
or is stored in plaintext.
"""
if isinstance(self.keystore, Hardware_KeyStore):
return STO_EV_XPUB_PW
else:
return STO_EV_USER_PW
def has_keystore_encryption(self):
"""Returns whether encryption is enabled for the keystore.
If True, e.g. signing a transaction will require a password.
"""
if self.can_have_keystore_encryption():
return self.storage.get('use_encryption', False)
return False
def has_storage_encryption(self):
"""Returns whether encryption is enabled for the wallet file on disk."""
return self.storage.is_encrypted()
@classmethod
def may_have_password(cls):
return True
def check_password(self, password):
self.keystore.check_password(password)
if self.has_keystore_encryption():
self.keystore.check_password(password)
self.storage.check_password(password)
def update_password(self, old_pw, new_pw, encrypt_storage=False):
if old_pw is None and self.has_password():
raise InvalidPassword()
self.check_password(old_pw)
if encrypt_storage:
enc_version = self.get_available_storage_encryption_version()
else:
enc_version = STO_EV_PLAINTEXT
self.storage.set_password(new_pw, enc_version)
# note: Encrypting storage with a hw device is currently only
# allowed for non-multisig wallets. Further,
# Hardware_KeyStore.may_have_password() == False.
# If these were not the case,
# extra care would need to be taken when encrypting keystores.
self._update_password_for_keystore(old_pw, new_pw)
encrypt_keystore = self.can_have_keystore_encryption()
self.storage.set_keystore_encryption(bool(new_pw) and encrypt_keystore)
self.storage.write()
def sign_message(self, address, message, password):
index = self.get_address_index(address)
@ -1386,16 +1441,10 @@ class Simple_Wallet(Abstract_Wallet):
def is_watching_only(self):
return self.keystore.is_watching_only()
def can_change_password(self):
return self.keystore.can_change_password()
def update_password(self, old_pw, new_pw, encrypt=False):
if old_pw is None and self.has_password():
raise InvalidPassword()
self.keystore.update_password(old_pw, new_pw)
self.save_keystore()
self.storage.set_password(new_pw, encrypt)
self.storage.write()
def _update_password_for_keystore(self, old_pw, new_pw):
if self.keystore and self.keystore.may_have_password():
self.keystore.update_password(old_pw, new_pw)
self.save_keystore()
def save_keystore(self):
self.storage.put('keystore', self.keystore.dump())
@ -1434,9 +1483,6 @@ class Imported_Wallet(Simple_Wallet):
def save_addresses(self):
self.storage.put('addresses', self.addresses)
def can_change_password(self):
return not self.is_watching_only()
def can_import_address(self):
return self.is_watching_only()
@ -1798,22 +1844,28 @@ class Multisig_Wallet(Deterministic_Wallet):
def get_keystores(self):
return [self.keystores[i] for i in sorted(self.keystores.keys())]
def update_password(self, old_pw, new_pw, encrypt=False):
if old_pw is None and self.has_password():
raise InvalidPassword()
def can_have_keystore_encryption(self):
return any([k.may_have_password() for k in self.get_keystores()])
def _update_password_for_keystore(self, old_pw, new_pw):
for name, keystore in self.keystores.items():
if keystore.can_change_password():
if keystore.may_have_password():
keystore.update_password(old_pw, new_pw)
self.storage.put(name, keystore.dump())
self.storage.set_password(new_pw, encrypt)
self.storage.write()
def check_password(self, password):
for name, keystore in self.keystores.items():
if keystore.may_have_password():
keystore.check_password(password)
self.storage.check_password(password)
def get_available_storage_encryption_version(self):
# multisig wallets are not offered hw device encryption
return STO_EV_USER_PW
def has_seed(self):
return self.keystore.has_seed()
def can_change_password(self):
return self.keystore.can_change_password()
def is_watching_only(self):
return not any([not k.is_watching_only() for k in self.get_keystores()])

View File

@ -194,7 +194,7 @@ class Plugin(BasePlugin):
return
wallet = window.wallet
if wallet.has_password():
if wallet.has_keystore_encryption():
password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.')
if not password:
return

View File

@ -12,7 +12,7 @@ try:
from electrum.keystore import Hardware_KeyStore
from ..hw_wallet import HW_PluginBase
from electrum.util import print_error, to_string, UserCancelled
from electrum.base_wizard import ScriptTypeNotSupported
from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
import time
import hid
@ -670,12 +670,13 @@ class DigitalBitboxPlugin(HW_PluginBase):
return None
def setup_device(self, device_info, wizard):
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
client.handler = self.create_handler(wizard)
client.setupRunning = True
if purpose == HWD_SETUP_NEW_WALLET:
client.setupRunning = True
client.get_xpub("m/44'/0'", 'standard')

View File

@ -65,9 +65,14 @@ class Plugin(BasePlugin):
tx = d.tx
wallet = d.wallet
window = d.main_window
if wallet.is_watching_only():
d.show_critical(_('This feature is not available for watch-only wallets.'))
return
# 1. get the password and sign the verification request
password = None
if wallet.has_password():
if wallet.has_keystore_encryption():
msg = _('GreenAddress requires your signature \n'
'to verify that transaction is instant.\n'
'Please enter your password to sign a\n'

View File

@ -51,3 +51,10 @@ class HW_PluginBase(BasePlugin):
for keystore in wallet.get_keystores():
if isinstance(keystore, self.keystore_class):
self.device_manager().unpair_xpub(keystore.xpub)
def setup_device(self, device_info, wizard, purpose):
"""Called when creating a new wallet or when using the device to decrypt
an existing wallet. Select the device to use. If the device is
uninitialized, go through the initialization process.
"""
raise NotImplementedError()

View File

@ -70,9 +70,10 @@ class QtHandlerBase(QObject, PrintError):
self.status_signal.emit(paired)
def _update_status(self, paired):
button = self.button
icon = button.icon_paired if paired else button.icon_unpaired
button.setIcon(QIcon(icon))
if hasattr(self, 'button'):
button = self.button
icon = button.icon_paired if paired else button.icon_unpaired
button.setIcon(QIcon(icon))
def query_choice(self, msg, labels):
self.done.clear()

View File

@ -194,10 +194,7 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
label, language)
wizard.loop.exit(0)
def setup_device(self, device_info, wizard):
'''Called when creating a new wallet. Select the device to use. If
the device is uninitialized, go through the intialization
process.'''
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)

View File

@ -522,7 +522,7 @@ class LedgerPlugin(HW_PluginBase):
client = Ledger_Client(client)
return client
def setup_device(self, device_info, wizard):
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)

View File

@ -214,10 +214,7 @@ class TrezorCompatiblePlugin(HW_PluginBase):
label, language)
wizard.loop.exit(0)
def setup_device(self, device_info, wizard):
'''Called when creating a new wallet. Select the device to use. If
the device is uninitialized, go through the intialization
process.'''
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)

View File

@ -40,6 +40,7 @@ from electrum.wallet import Multisig_Wallet, Deterministic_Wallet
from electrum.i18n import _
from electrum.plugins import BasePlugin, hook
from electrum.util import NotEnoughFunds
from electrum.storage import STO_EV_USER_PW
# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server
signing_xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
@ -420,9 +421,11 @@ class TrustedCoinPlugin(BasePlugin):
k2 = keystore.from_xpub(xpub2)
wizard.request_password(run_next=lambda pw, encrypt: self.on_password(wizard, pw, encrypt, k1, k2))
def on_password(self, wizard, password, encrypt, k1, k2):
def on_password(self, wizard, password, encrypt_storage, k1, k2):
k1.update_password(None, password)
wizard.storage.set_password(password, encrypt)
wizard.storage.set_keystore_encryption(bool(password))
if encrypt_storage:
wizard.storage.set_password(password, enc_version=STO_EV_USER_PW)
wizard.storage.put('x1/', k1.dump())
wizard.storage.put('x2/', k2.dump())
wizard.storage.write()
@ -470,7 +473,7 @@ class TrustedCoinPlugin(BasePlugin):
else:
self.create_keystore(wizard, seed, passphrase)
def on_restore_pw(self, wizard, seed, passphrase, password, encrypt):
def on_restore_pw(self, wizard, seed, passphrase, password, encrypt_storage):
storage = wizard.storage
xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase)
k1 = keystore.from_xprv(xprv1)
@ -484,7 +487,11 @@ class TrustedCoinPlugin(BasePlugin):
xpub3 = make_xpub(signing_xpub, long_user_id)
k3 = keystore.from_xpub(xpub3)
storage.put('x3/', k3.dump())
storage.set_password(password, encrypt)
storage.set_keystore_encryption(bool(password))
if encrypt_storage:
storage.set_password(password, enc_version=STO_EV_USER_PW)
wizard.wallet = Wallet_2fa(storage)
wizard.create_addresses()