Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8308343
Update btchip dependency
harzo Dec 8, 2020
37dc6f4
Add Ledger 2 Keys wallet creation flow
harzo Dec 16, 2020
47d83e1
Update synchronization flow for hw
harzo Dec 16, 2020
f3064ef
Create 2 and 3 keys HW wallets classes
harzo Dec 16, 2020
f09c05a
Use multiple keystores
harzo Dec 22, 2020
d89c5c3
Use alert/instant/recovery pubkey derivation parameter in getWalletPu…
mgrychow Dec 30, 2020
d6e237b
Use p2sh for ledger
harzo Jan 4, 2021
9e754b2
Revert "Use p2sh for ledger"
harzo Jan 4, 2021
2bd8eeb
Sign alert tx with ledger
harzo Jan 4, 2021
3462d03
Change default derivation path for ledger
harzo Jan 4, 2021
9f5ae45
Ask for password confirmation in ledger btcv wallet creation
mgrychow Jan 4, 2021
1fa8144
Ledger 3-keys HW wallet creation
mgrychow Jan 5, 2021
9330ef9
Fix witness checksum missmatch
harzo Jan 5, 2021
3b1477a
Update set_password_use method
harzo Jan 7, 2021
bece114
Importing 2-keys and 3-keys wallets from Ledger - asking user for pas…
mgrychow Jan 11, 2021
bb809f3
Check recovery password saved on stick on 2-keys wallet import
mgrychow Jan 29, 2021
9599249
Check recovery and instant passwords saved on stick on 3-keys wallet …
mgrychow Jan 29, 2021
915979e
Btcv wallet import cornercases: setting and checking wallet type info…
mgrychow Feb 1, 2021
d5482b1
signing 3-keys alert transaction on ledger
mgrychow Feb 7, 2021
158c9a0
Ledger signing 2-keys recovery transaction with hardcoded recovery pa…
mgrychow Feb 23, 2021
879da00
User provided recovery password check for 2-keys recovery tx signed b…
mgrychow Feb 24, 2021
6437aa5
3-keys recovery transaction signed by ledger
mgrychow Feb 26, 2021
d271049
3-keys instant transaction signed by ledger
mgrychow Mar 1, 2021
0d5bbec
Fix for >2 output btcv multi-key tx signing on ledger device
mgrychow Mar 2, 2021
6094a86
Rework 1 for feature/ledger-integration merge
mgrychow Mar 10, 2021
49d619e
update btchip-python dependency version tag
mgrychow Mar 11, 2021
8cb7db9
Rework 2 for feature/ledger-integration merge
mgrychow Mar 15, 2021
abef57c
Backward compatibility with standard wallet on bitcoin ledger app
mgrychow Mar 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Run install (this should install dependencies)::

python3 -m pip install .[fast]

In case of problems with btchip library::

pip install git+https://git@github.com/bitcoinvault/btchip-python.git@v0.1.31-btcv

Compile the protobuf description file::

Expand Down
2 changes: 1 addition & 1 deletion contrib/requirements/requirements-hw.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
trezor[hidapi]>=0.11.5
safet>=0.1.5
keepkey>=6.0.3
btchip-python>=0.1.26
btchip-python@git://github.com/bitcoinvault/btchip-python.git@v0.1.31-btcv
ckcc-protocol>=0.7.7
hidapi
140 changes: 121 additions & 19 deletions electrum/base_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import sys
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional

from btchip.btchipException import BTChipException

from . import bitcoin
from . import keystore
from . import mnemonic
Expand All @@ -41,15 +43,18 @@
from .simple_config import SimpleConfig
from .storage import (WalletStorage, StorageEncryptionVersion,
get_derivation_used_for_hw_device_encryption)
from .three_keys import short_mnemonic
from .three_keys.pubkey_type import PubkeyType
from .util import UserCancelled, InvalidPassword
from .wallet import (wallet_types)

if TYPE_CHECKING:
from .plugin import DeviceInfo

# hardware device setup purpose
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
HWD_SETUP_NEW_WALLET = 0
HWD_SETUP_DECRYPT_WALLET = 1
HWD_SETUP_NEW_BTCV_WALLET = 2
HWD_SETUP_DECRYPT_BTCV_WALLET = 3


class ScriptTypeNotSupported(Exception): pass
Expand Down Expand Up @@ -215,6 +220,17 @@ def process_choice(choice):
action = 'three_keys_2fa' + sub_action
else:
raise Exception('Invalid multikey wallet type: ' + self.wallet_type)
elif choice[:11] == 'multikey_hw':
self.data['multikey_type'] = 'hw'
self.data['wallet_type'] += '-hw'
self.wallet_type += '-hw'
sub_action = choice[-7:]
if self.wallet_type == '2-key-hw':
action = 'two_keys_hw' + sub_action
elif self.wallet_type == '3-key-hw':
action = 'three_keys_hw' + sub_action
else:
raise Exception('Invalid multikey wallet type: ' + self.wallet_type)
else:
raise Exception('Invalid choice: ' + choice)
self.run(action)
Expand All @@ -228,7 +244,9 @@ def process_choice(choice):
choices = [
('multikey_2fa_create', _('Use Gold Wallet and create a new wallet')),
('multikey_2fa_import', _('Use Gold Wallet and import an existing wallet')),
('multikey_standalone', _('Do not use Gold Wallet')),
('multikey_hw_create', _('Use Ledger device and create a new wallet')),
('multikey_hw_import', _('Use Ledger device and import an existing wallet')),
('multikey_standalone', _('Do not use Gold Wallet nor Ledger device')),
]

self.choice_dialog(title=title, message=message, choices=choices, run_next=process_choice)
Expand All @@ -242,6 +260,13 @@ def two_keys_2fa_create(self):
def two_keys_2fa_import(self):
self.get_authenticator_pubkey(run_next=self.on_two_keys_import)

def two_keys_hw_create(self):
self.get_hw_password(run_next=self.on_two_keys_hw_create, title=_('Cancel password'))

def two_keys_hw_import(self):
self.check_hw_password(run_next=self.on_two_keys_hw_import, title=_('Cancel password'))


def on_two_keys_create(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('choose_keystore')
Expand All @@ -250,6 +275,14 @@ def on_two_keys_import(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('restore_from_seed')

def on_two_keys_hw_create(self, recovery_password: str):
self.data['recovery_password'] = recovery_password
self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET)

def on_two_keys_hw_import(self, recovery_password: str):
self.data['recovery_password'] = recovery_password
self.run('choose_hw_device', HWD_SETUP_DECRYPT_BTCV_WALLET)

def three_keys_standalone(self):
def collect_instant_pubkey(instant_pubkey: str):
self.data['instant_pubkey'] = instant_pubkey
Expand All @@ -271,6 +304,12 @@ def collect_instant_pubkey(instant_pubkey: str):

self.get_authenticator_pubkey(run_next=collect_instant_pubkey)

def three_keys_hw_create(self):
self.get_hw_passwords(run_next=self.on_three_keys_hw_create, title=_('Instant and cancel passwords'))

def three_keys_hw_import(self):
self.check_hw_passwords(run_next=self.on_three_keys_hw_import, title=_('Instant and cancel passwords'))

def on_three_keys_create(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('choose_keystore')
Expand All @@ -279,8 +318,18 @@ def on_three_keys_import(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('restore_from_seed')

def on_three_keys_hw_create(self, *passwords):
self.data['instant_password'] = passwords[0]
self.data['recovery_password'] = passwords[1]
self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET)

def on_three_keys_hw_import(self, *passwords):
self.data['instant_password'] = passwords[0]
self.data['recovery_password'] = passwords[1]
self.run('choose_hw_device', HWD_SETUP_DECRYPT_BTCV_WALLET)

def choose_keystore(self):
assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key']
assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key', '2-key-hw', '3-key-hw']
i = len(self.keystores)
title = _('Add cosigner') + ' (%d of %d)' % (i + 1, self.n) if self.wallet_type == 'multisig' else _('Keystore')
if self.wallet_type == 'multisig' and i > 0:
Expand All @@ -304,7 +353,7 @@ def choose_keystore(self):
advanced_choices = [
('restore_from_key', _('Use a master key')),
]
if not self.is_kivy and self.wallet_type not in ['2-key', '3-key']:
if not self.is_kivy and self.wallet_type not in ['2-key', '3-key', '2-key-hw', '3-key-hw']:
advanced_choices.append(('choose_hw_device', _('Use a hardware device')))
if self.wallet_type == 'multisig':
self.choice_dialog(title=title, message=message, choices=base_choices + advanced_choices, run_next=self.run)
Expand Down Expand Up @@ -489,6 +538,28 @@ def f(derivation, script_type):
if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this
client.clear_session()
raise
elif purpose == HWD_SETUP_NEW_BTCV_WALLET:
if 'recovery_password' not in self.data:
raise Exception('Recovery password not set')

self.plugin.set_recovery_password(device_info.device.id_, self.data['recovery_password'], self)
if 'instant_password' in self.data:
self.plugin.set_instant_password(device_info.device.id_, self.data['instant_password'], self)
else:
self.plugin.set_instant_password(device_info.device.id_, str(0x00), self)

self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh')
elif purpose == HWD_SETUP_DECRYPT_BTCV_WALLET:
if 'recovery_password' not in self.data:
raise Exception('Invalid recovery password')
Comment thread
mgrychow marked this conversation as resolved.
if 'instant_password' in self.data:
Comment thread
mgrychow marked this conversation as resolved.
self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh',
btcv_instant_password_check=self.data['instant_password'],
btcv_recovery_password_check=self.data['recovery_password'])
else:
self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh',
btcv_recovery_password_check=self.data['recovery_password'],
btcv_instant_password_check=str(0x00))
else:
raise Exception('unknown purpose: %s' % purpose)

Expand Down Expand Up @@ -526,10 +597,11 @@ def derivation_and_script_type_dialog(self, f):
self.show_error(e)
# let the user choose again

def on_hw_derivation(self, name, device_info, derivation, xtype):
def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False, pubkey_type=PubkeyType.PUBKEY_ALERT,
btcv_instant_password_check=None, btcv_recovery_password_check=None):
from .keystore import hardware_keystore
try:
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self, pubkey_type)
root_xpub = self.plugin.get_xpub(device_info.device.id_, 'm', 'standard', self)
except ScriptTypeNotSupported:
raise # this is handled in derivation_dialog
Expand All @@ -538,16 +610,24 @@ def on_hw_derivation(self, name, device_info, derivation, xtype):
self.show_error(e)
return
xfp = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower()
d = {
'type': 'hardware',
'hw_type': name,
'derivation': derivation,
'root_fingerprint': xfp,
'xpub': xpub,
'label': device_info.label,
}
k = hardware_keystore(d)
self.on_keystore(k)
if xpub_keystore:
k = keystore.from_master_key(xpub)
else:
d = {
'type': 'hardware',
'hw_type': name,
'derivation': derivation,
'root_fingerprint': xfp,
'xpub': xpub,
'label': device_info.label,
}
k = hardware_keystore(d)
from electrum.plugins.ledger.ledger import Ledger_KeyStore
if isinstance(k, Ledger_KeyStore) \
Comment thread
mgrychow marked this conversation as resolved.
and not k.are_3keys_ledger_passwords_correct(self, btcv_instant_password_check, btcv_recovery_password_check):
# user already notified about invalid password(s)
return self.terminate()
self.on_keystore(k, name, device_info)

def passphrase_dialog(self, run_next, is_restoring=False):
title = _('Seed extension')
Expand Down Expand Up @@ -609,7 +689,7 @@ def on_bip43(self, seed, passphrase, derivation, script_type):
k = keystore.from_bip39_seed(seed, passphrase, derivation, xtype=script_type)
self.on_keystore(k)

def on_keystore(self, k):
def on_keystore(self, k, name=None, device_info=None):
has_xpub = isinstance(k, keystore.Xpub)
if has_xpub:
t1 = xpub_type(k.xpub)
Expand All @@ -620,6 +700,28 @@ def on_keystore(self, k):
return
self.keystores.append(k)
self.run('create_wallet')
elif self.wallet_type in ['2-key-hw', '3-key-hw']:
if has_xpub and t1 != 'p2wsh-p2sh':
self.show_error(_('Wrong key type') + ' %s' % t1)
self.run('choose_keystore')
return
self.keystores.append(k)

keystores_needed = 2 if self.wallet_type == '2-key-hw' else 3
if len(self.keystores) < keystores_needed:
if not name or not device_info:
self.show_error(_('Missing device info'))
self.run('choose_keystore')
return
script_type = 'p2wsh-p2sh'
derivation = bip44_derivation(0)
if keystores_needed == 3:
pubkey_type = len(self.keystores)
elif keystores_needed == 2:
pubkey_type = 2 * len(self.keystores)
self.run('on_hw_derivation', name, device_info, derivation, script_type, True, pubkey_type)
else:
self.run('create_wallet')
elif self.wallet_type == 'multisig':
assert has_xpub
if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:
Expand Down Expand Up @@ -694,7 +796,7 @@ def on_password(self, password, *, encrypt_storage: bool,
self.data['seed_type'] = self.seed_type
keys = self.keystores[0].dump()
self.data['keystore'] = keys
elif self.wallet_type == 'multisig':
elif self.wallet_type in ['multisig', '2-key-hw', '3-key-hw']:
for i, k in enumerate(self.keystores):
self.data['x%d/' % (i + 1)] = k.dump()
elif self.wallet_type == 'imported':
Expand Down
6 changes: 5 additions & 1 deletion electrum/gui/qt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from typing import Optional, TYPE_CHECKING

from .terms_and_conditions_mixin import TermsNotAccepted
from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow
from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow, ElectrumARHWWindow, ElectrumAIRHWWindow

try:
import PyQt5
Expand Down Expand Up @@ -211,6 +211,10 @@ def _create_window_for_wallet(self, wallet):
w = ElectrumARWindow(self, wallet)
elif wallet_type == '3-key':
w = ElectrumAIRWindow(self, wallet)
elif wallet_type == '2-key-hw':
w = ElectrumARHWWindow(self, wallet)
elif wallet_type == '3-key-hw':
w = ElectrumAIRHWWindow(self, wallet)
else:
w = ElectrumWindow(self, wallet)
self.windows.append(w)
Expand Down
80 changes: 79 additions & 1 deletion electrum/gui/qt/installwizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
from .seed_dialog import SeedLayout, KeysLayout
from .terms_and_conditions_mixin import TermsAndConditionsMixin, PushedButton
from .three_keys_dialogs import InsertPubKeyDialog, Qr2FaDialog
from .three_keys_dialogs import InsertPubKeyDialog, InsertHWPasswordDialog, Qr2FaDialog, CheckHWPasswordDialog
from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel,
InfoButton, char_width_in_lineedit, get_default_language)

Expand Down Expand Up @@ -651,6 +651,84 @@ def get_authenticator_pubkey(self, run_next, disallowed_key=None):
self.exec_layout(layout, _('Gold Wallet authenticator public key'), next_enabled=False)
return layout.get_compressed_pubkey()

@wizard_dialog
def get_hw_password(self, run_next, title):
label = QLabel()
message = _('Please provide password related to your Ledger device')
label.setText(message)
label.setOpenExternalLinks(True)
label.setTextInteractionFlags(Qt.TextBrowserInteraction)
label.setWordWrap(True)

layout = InsertHWPasswordDialog(self, message_label=label)
self.exec_layout(layout, title, next_enabled=False)
return layout.get_password()

@wizard_dialog
def get_hw_passwords(self, run_next, title):
label1 = QLabel()
message1 = _('Please provide <b>instant</b> password related to your Ledger device')
label1.setText(message1)
label1.setOpenExternalLinks(True)
label1.setTextInteractionFlags(Qt.TextBrowserInteraction)
label1.setWordWrap(True)

layout1 = InsertHWPasswordDialog(self, message_label=label1)
self.exec_layout(layout1, title, next_enabled=False)
instant_password = layout1.get_password()

label2 = QLabel()
message2 = _('Please provide <b>cancel</b> password related to your Ledger device')
label2.setText(message2)
label2.setOpenExternalLinks(True)
label2.setTextInteractionFlags(Qt.TextBrowserInteraction)
label2.setWordWrap(True)

layout2 = InsertHWPasswordDialog(self, message_label=label2)
self.exec_layout(layout2, title, next_enabled=False)
recovery_password = layout2.get_password()

return (instant_password, recovery_password)

@wizard_dialog
def check_hw_password(self, run_next, title):
label = QLabel()
message = _('Please provide password related to your Ledger device')
label.setText(message)
label.setOpenExternalLinks(True)
label.setTextInteractionFlags(Qt.TextBrowserInteraction)
label.setWordWrap(True)

layout = CheckHWPasswordDialog(self, message_label=label)
self.exec_layout(layout, title, next_enabled=False)
return layout.get_password()

@wizard_dialog
def check_hw_passwords(self, run_next, title):
label1 = QLabel()
message1 = _('Please provide <b>instant</b> password related to your Ledger device')
label1.setText(message1)
label1.setOpenExternalLinks(True)
label1.setTextInteractionFlags(Qt.TextBrowserInteraction)
label1.setWordWrap(True)

layout1 = CheckHWPasswordDialog(self, message_label=label1)
self.exec_layout(layout1, title, next_enabled=False)
instant_password = layout1.get_password()

label2 = QLabel()
message2 = _('Please provide <b>cancel</b> password related to your Ledger device')
label2.setText(message2)
label2.setOpenExternalLinks(True)
label2.setTextInteractionFlags(Qt.TextBrowserInteraction)
label2.setWordWrap(True)

layout2 = CheckHWPasswordDialog(self, message_label=label2)
self.exec_layout(layout2, title, next_enabled=False)
recovery_password = layout2.get_password()

return (instant_password, recovery_password)

@wizard_dialog
def display_2fa_pairing_qr(self, run_next, entropy: bytes):
title_label = QLabel()
Expand Down
Loading