ledger new ui and mobile 2fa validation
This commit is contained in:
parent
59ed5932a8
commit
da7e48f3a7
|
@ -0,0 +1,347 @@
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QDialog, QInputDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel, SIGNAL)
|
||||||
|
import PyQt4.QtCore as QtCore
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum_gui.qt.util import *
|
||||||
|
from electrum.util import print_msg
|
||||||
|
|
||||||
|
import os, hashlib, websocket, threading, logging, json, copy
|
||||||
|
from electrum_gui.qt.qrcodewidget import QRCodeWidget, QRDialog
|
||||||
|
from btchip.btchip import *
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
helpTxt = [_("Your Ledger Wallet wants tell you a one-time PIN code.<br><br>" \
|
||||||
|
"For best security you should unplug your device, open a text editor on another computer, " \
|
||||||
|
"put your cursor into it, and plug your device into that computer. " \
|
||||||
|
"It will output a summary of the transaction being signed and a one-time PIN.<br><br>" \
|
||||||
|
"Verify the transaction summary and type the PIN code here.<br><br>" \
|
||||||
|
"Before pressing enter, plug the device back into this computer.<br>" ),
|
||||||
|
_("Verify the address below.<br>Type the character from your security card corresponding to the <u><b>BOLD</b></u> character."),
|
||||||
|
_("Waiting for authentication on your mobile phone"),
|
||||||
|
_("Transaction accepted by mobile phone. Waiting for confirmation."),
|
||||||
|
_("Click Pair button to begin pairing a mobile phone."),
|
||||||
|
_("Scan this QR code with your LedgerWallet phone app to pair it with this Ledger device.<br>"
|
||||||
|
"To complete pairing you will need your security card to answer a challenge." )
|
||||||
|
]
|
||||||
|
|
||||||
|
class LedgerAuthDialog(QDialog):
|
||||||
|
def __init__(self, handler, data):
|
||||||
|
'''Ask user for 2nd factor authentication. Support text, security card and paired mobile methods.
|
||||||
|
Use last method from settings, but support new pairing and downgrade.
|
||||||
|
'''
|
||||||
|
QDialog.__init__(self, handler.top_level_window())
|
||||||
|
self.handler = handler
|
||||||
|
self.txdata = data
|
||||||
|
self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else ''
|
||||||
|
self.setMinimumWidth(600)
|
||||||
|
self.setWindowTitle(_("Ledger Wallet Authentication"))
|
||||||
|
self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg)
|
||||||
|
self.dongle = self.handler.win.wallet.get_keystore().get_client().dongle
|
||||||
|
self.ws = None
|
||||||
|
self.pin = ''
|
||||||
|
|
||||||
|
self.devmode = self.getDevice2FAMode()
|
||||||
|
if self.devmode == 0x11 or self.txdata['confirmationType'] == 1:
|
||||||
|
self.cfg['mode'] = 0
|
||||||
|
|
||||||
|
vbox = QVBoxLayout()
|
||||||
|
self.setLayout(vbox)
|
||||||
|
|
||||||
|
def on_change_mode(idx):
|
||||||
|
if idx < 2 and self.ws:
|
||||||
|
self.ws.stop()
|
||||||
|
self.ws = None
|
||||||
|
self.cfg['mode'] = 0 if self.devmode == 0x11 else idx if idx > 0 else 1
|
||||||
|
if self.cfg['mode'] > 1 and self.cfg['pair'] and not self.ws:
|
||||||
|
self.req_validation()
|
||||||
|
if self.cfg['mode'] > 0:
|
||||||
|
self.handler.win.wallet.get_keystore().cfg = self.cfg
|
||||||
|
self.handler.win.wallet.save_keystore()
|
||||||
|
self.update_dlg()
|
||||||
|
def add_pairing():
|
||||||
|
self.do_pairing()
|
||||||
|
def return_pin():
|
||||||
|
self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text()
|
||||||
|
if self.cfg['mode'] == 1:
|
||||||
|
self.pin = ''.join(chr(int(str(i),16)) for i in self.pin)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
self.modebox = QWidget()
|
||||||
|
modelayout = QHBoxLayout()
|
||||||
|
self.modebox.setLayout(modelayout)
|
||||||
|
modelayout.addWidget(QLabel(_("Method:")))
|
||||||
|
self.modes = QComboBox()
|
||||||
|
modelayout.addWidget(self.modes, 2)
|
||||||
|
self.addPair = QPushButton(_("Pair"))
|
||||||
|
self.addPair.setMaximumWidth(60)
|
||||||
|
modelayout.addWidget(self.addPair)
|
||||||
|
modelayout.addStretch(1)
|
||||||
|
self.modebox.setMaximumHeight(50)
|
||||||
|
vbox.addWidget(self.modebox)
|
||||||
|
|
||||||
|
self.populate_modes()
|
||||||
|
self.modes.currentIndexChanged.connect(on_change_mode)
|
||||||
|
self.addPair.clicked.connect(add_pairing)
|
||||||
|
|
||||||
|
self.helpmsg = QTextEdit()
|
||||||
|
self.helpmsg.setStyleSheet("QTextEdit { background-color: lightgray; }")
|
||||||
|
self.helpmsg.setReadOnly(True)
|
||||||
|
vbox.addWidget(self.helpmsg)
|
||||||
|
|
||||||
|
self.pinbox = QWidget()
|
||||||
|
pinlayout = QHBoxLayout()
|
||||||
|
self.pinbox.setLayout(pinlayout)
|
||||||
|
self.pintxt = QLineEdit()
|
||||||
|
self.pintxt.setEchoMode(2)
|
||||||
|
self.pintxt.setMaxLength(4)
|
||||||
|
self.pintxt.returnPressed.connect(return_pin)
|
||||||
|
pinlayout.addWidget(QLabel(_("Enter PIN:")))
|
||||||
|
pinlayout.addWidget(self.pintxt)
|
||||||
|
pinlayout.addWidget(QLabel(_("NOT DEVICE PIN - see above")))
|
||||||
|
pinlayout.addStretch(1)
|
||||||
|
self.pinbox.setVisible(self.cfg['mode'] == 0)
|
||||||
|
vbox.addWidget(self.pinbox)
|
||||||
|
|
||||||
|
self.cardbox = QWidget()
|
||||||
|
card = QVBoxLayout()
|
||||||
|
self.cardbox.setLayout(card)
|
||||||
|
self.addrtext = QTextEdit()
|
||||||
|
self.addrtext.setStyleSheet("QTextEdit { color:blue; background-color:lightgray; padding:15px 10px; border:none; font-size:20pt; }")
|
||||||
|
self.addrtext.setReadOnly(True)
|
||||||
|
self.addrtext.setMaximumHeight(120)
|
||||||
|
card.addWidget(self.addrtext)
|
||||||
|
|
||||||
|
def pin_changed(s):
|
||||||
|
if len(s) < len(self.idxs):
|
||||||
|
i = self.idxs[len(s)]
|
||||||
|
addr = self.txdata['address']
|
||||||
|
addr = addr[:i] + '<u><b>' + addr[i:i+1] + '</u></b>' + addr[i+1:]
|
||||||
|
self.addrtext.setHtml(str(addr))
|
||||||
|
else:
|
||||||
|
self.addrtext.setHtml(_("Press Enter"))
|
||||||
|
|
||||||
|
pin_changed('')
|
||||||
|
cardpin = QHBoxLayout()
|
||||||
|
cardpin.addWidget(QLabel(_("Enter PIN:")))
|
||||||
|
self.cardtxt = QLineEdit()
|
||||||
|
self.cardtxt.setEchoMode(2)
|
||||||
|
self.cardtxt.setMaxLength(len(self.idxs))
|
||||||
|
self.cardtxt.textChanged.connect(pin_changed)
|
||||||
|
self.cardtxt.returnPressed.connect(return_pin)
|
||||||
|
cardpin.addWidget(self.cardtxt)
|
||||||
|
cardpin.addWidget(QLabel(_("NOT DEVICE PIN - see above")))
|
||||||
|
cardpin.addStretch(1)
|
||||||
|
card.addLayout(cardpin)
|
||||||
|
self.cardbox.setVisible(self.cfg['mode'] == 1)
|
||||||
|
vbox.addWidget(self.cardbox)
|
||||||
|
|
||||||
|
self.pairbox = QWidget()
|
||||||
|
pairlayout = QVBoxLayout()
|
||||||
|
self.pairbox.setLayout(pairlayout)
|
||||||
|
pairhelp = QTextEdit(helpTxt[5])
|
||||||
|
pairhelp.setStyleSheet("QTextEdit { background-color: lightgray; }")
|
||||||
|
pairhelp.setReadOnly(True)
|
||||||
|
pairlayout.addWidget(pairhelp, 1)
|
||||||
|
self.pairqr = QRCodeWidget()
|
||||||
|
pairlayout.addWidget(self.pairqr, 4)
|
||||||
|
self.pairbox.setVisible(False)
|
||||||
|
vbox.addWidget(self.pairbox)
|
||||||
|
self.update_dlg()
|
||||||
|
|
||||||
|
if self.cfg['mode'] > 1 and not self.ws:
|
||||||
|
self.req_validation()
|
||||||
|
|
||||||
|
def populate_modes(self):
|
||||||
|
self.modes.blockSignals(True)
|
||||||
|
self.modes.clear()
|
||||||
|
self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled"))
|
||||||
|
if self.txdata['confirmationType'] > 1:
|
||||||
|
self.modes.addItem(_("Security Card Challenge"))
|
||||||
|
if not self.cfg['pair']:
|
||||||
|
self.modes.addItem(_("Mobile - Not paired"))
|
||||||
|
else:
|
||||||
|
self.modes.addItem(_("Mobile - %s") % self.cfg['pair'][1])
|
||||||
|
self.modes.blockSignals(False)
|
||||||
|
|
||||||
|
def update_dlg(self):
|
||||||
|
self.modes.setCurrentIndex(self.cfg['mode'])
|
||||||
|
self.modebox.setVisible(True)
|
||||||
|
self.addPair.setText(_("Pair") if not self.cfg['pair'] else _("Re-Pair"))
|
||||||
|
self.addPair.setVisible(self.txdata['confirmationType'] > 2)
|
||||||
|
self.helpmsg.setText(helpTxt[self.cfg['mode'] if self.cfg['mode'] < 2 else 2 if self.cfg['pair'] else 4])
|
||||||
|
self.helpmsg.setMinimumHeight(180 if self.txdata['confirmationType'] == 1 else 100)
|
||||||
|
self.pairbox.setVisible(False)
|
||||||
|
self.helpmsg.setVisible(True)
|
||||||
|
self.pinbox.setVisible(self.cfg['mode'] == 0)
|
||||||
|
self.cardbox.setVisible(self.cfg['mode'] == 1)
|
||||||
|
self.pintxt.setFocus(True) if self.cfg['mode'] == 0 else self.cardtxt.setFocus(True)
|
||||||
|
self.setMaximumHeight(200)
|
||||||
|
|
||||||
|
def do_pairing(self):
|
||||||
|
rng = os.urandom(16)
|
||||||
|
pairID = rng.encode('hex') + hashlib.sha256(rng).digest()[0].encode('hex')
|
||||||
|
self.pairqr.setData(pairID)
|
||||||
|
self.modebox.setVisible(False)
|
||||||
|
self.helpmsg.setVisible(False)
|
||||||
|
self.pinbox.setVisible(False)
|
||||||
|
self.cardbox.setVisible(False)
|
||||||
|
self.pairbox.setVisible(True)
|
||||||
|
self.pairqr.setMinimumSize(300,300)
|
||||||
|
if self.ws:
|
||||||
|
self.ws.stop()
|
||||||
|
self.ws = LedgerWebSocket(self, pairID)
|
||||||
|
self.ws.pairing_done.connect(self.pairing_done)
|
||||||
|
self.ws.start()
|
||||||
|
|
||||||
|
def pairing_done(self, data):
|
||||||
|
if data is not None:
|
||||||
|
self.cfg['pair'] = [ data['pairid'], data['name'], data['platform'] ]
|
||||||
|
self.cfg['mode'] = 2
|
||||||
|
self.handler.win.wallet.get_keystore().cfg = self.cfg
|
||||||
|
self.handler.win.wallet.save_keystore()
|
||||||
|
self.pin = 'paired'
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def req_validation(self):
|
||||||
|
if self.cfg['pair'] and 'secureScreenData' in self.txdata:
|
||||||
|
if self.ws:
|
||||||
|
self.ws.stop()
|
||||||
|
self.ws = LedgerWebSocket(self, self.cfg['pair'][0], self.txdata)
|
||||||
|
self.ws.req_updated.connect(self.req_updated)
|
||||||
|
self.ws.start()
|
||||||
|
|
||||||
|
def req_updated(self, pin):
|
||||||
|
if pin == 'accepted':
|
||||||
|
self.helpmsg.setText(helpTxt[3])
|
||||||
|
else:
|
||||||
|
self.pin = str(pin)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def getDevice2FAMode(self):
|
||||||
|
apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode
|
||||||
|
try:
|
||||||
|
mode = self.dongle.exchange( bytearray(apdu) )
|
||||||
|
return mode
|
||||||
|
except BTChipException, e:
|
||||||
|
debug_msg('Device getMode Failed')
|
||||||
|
return 0x11
|
||||||
|
|
||||||
|
def closeEvent(self, evnt):
|
||||||
|
debug_msg("CLOSE - Stop WS")
|
||||||
|
if self.ws:
|
||||||
|
self.ws.stop()
|
||||||
|
if self.pairbox.isVisible():
|
||||||
|
evnt.ignore()
|
||||||
|
self.update_dlg()
|
||||||
|
|
||||||
|
class LedgerWebSocket(QThread):
|
||||||
|
pairing_done = pyqtSignal(object)
|
||||||
|
req_updated = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, dlg, pairID, txdata=None):
|
||||||
|
QThread.__init__(self)
|
||||||
|
self.stopping = False
|
||||||
|
self.pairID = pairID
|
||||||
|
self.txreq = '{"type":"request","second_factor_data":"' + str(txdata['secureScreenData']).encode('hex') + '"}' if txdata else None
|
||||||
|
self.dlg = dlg
|
||||||
|
self.dongle = self.dlg.dongle
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
#websocket.enableTrace(True)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
self.ws = websocket.WebSocketApp('wss://ws.ledgerwallet.com/2fa/channels',
|
||||||
|
on_message = self.on_message, on_error = self.on_error,
|
||||||
|
on_close = self.on_close, on_open = self.on_open)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not self.stopping:
|
||||||
|
self.ws.run_forever()
|
||||||
|
def stop(self):
|
||||||
|
debug_msg("WS: Stopping")
|
||||||
|
self.stopping = True
|
||||||
|
self.ws.close()
|
||||||
|
|
||||||
|
def on_message(self, ws, msg):
|
||||||
|
data = json.loads(msg)
|
||||||
|
if data['type'] == 'identify':
|
||||||
|
debug_msg('Identify')
|
||||||
|
apdu = [0xe0, 0x12, 0x01, 0x00, 0x41] # init pairing
|
||||||
|
apdu.extend(data['public_key'].decode('hex'))
|
||||||
|
try:
|
||||||
|
challenge = self.dongle.exchange( bytearray(apdu) )
|
||||||
|
ws.send( '{"type":"challenge","data":"%s" }' % str(challenge).encode('hex') )
|
||||||
|
self.data = data
|
||||||
|
except BTChipException, e:
|
||||||
|
debug_msg('Identify Failed')
|
||||||
|
|
||||||
|
if data['type'] == 'challenge':
|
||||||
|
debug_msg('Challenge')
|
||||||
|
apdu = [0xe0, 0x12, 0x02, 0x00, 0x10] # confirm pairing
|
||||||
|
apdu.extend(data['data'].decode('hex'))
|
||||||
|
try:
|
||||||
|
self.dongle.exchange( bytearray(apdu) )
|
||||||
|
debug_msg('Pairing Successful')
|
||||||
|
ws.send( '{"type":"pairing","is_successful":"true"}' )
|
||||||
|
self.data['pairid'] = self.pairID
|
||||||
|
self.pairing_done.emit(self.data)
|
||||||
|
except BTChipException, e:
|
||||||
|
debug_msg('Pairing Failed')
|
||||||
|
ws.send( '{"type":"pairing","is_successful":"false"}' )
|
||||||
|
self.pairing_done.emit(None)
|
||||||
|
ws.send( '{"type":"disconnect"}' )
|
||||||
|
self.stopping = True
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
if data['type'] == 'accept':
|
||||||
|
debug_msg('Accepted')
|
||||||
|
self.req_updated.emit('accepted')
|
||||||
|
if data['type'] == 'response':
|
||||||
|
debug_msg('Responded', data)
|
||||||
|
self.req_updated.emit(str(data['pin']) if data['is_accepted'] else '')
|
||||||
|
self.txreq = None
|
||||||
|
self.stopping = True
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
if data['type'] == 'repeat':
|
||||||
|
debug_msg('Repeat')
|
||||||
|
if self.txreq:
|
||||||
|
ws.send( self.txreq )
|
||||||
|
debug_msg("Req Sent", self.txreq)
|
||||||
|
if data['type'] == 'connect':
|
||||||
|
debug_msg('Connected')
|
||||||
|
if self.txreq:
|
||||||
|
ws.send( self.txreq )
|
||||||
|
debug_msg("Req Sent", self.txreq)
|
||||||
|
if data['type'] == 'disconnect':
|
||||||
|
debug_msg('Disconnected')
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
def on_error(self, ws, error):
|
||||||
|
message = getattr(error, 'strerror', '')
|
||||||
|
if not message:
|
||||||
|
message = getattr(error, 'message', '')
|
||||||
|
debug_msg("WS: %s" % message)
|
||||||
|
|
||||||
|
def on_close(self, ws):
|
||||||
|
debug_msg("WS: ### socket closed ###")
|
||||||
|
|
||||||
|
def on_open(self, ws):
|
||||||
|
debug_msg("WS: ### socket open ###")
|
||||||
|
debug_msg("Joining with pairing ID", self.pairID)
|
||||||
|
ws.send( '{"type":"join","room":"%s"}' % self.pairID )
|
||||||
|
ws.send( '{"type":"repeat"}' )
|
||||||
|
if self.txreq:
|
||||||
|
ws.send( self.txreq )
|
||||||
|
debug_msg("Req Sent", self.txreq)
|
||||||
|
|
||||||
|
def debug_msg(*args):
|
||||||
|
if DEBUG:
|
||||||
|
print_msg(*args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,12 @@ from electrum.keystore import Hardware_KeyStore, parse_xpubkey
|
||||||
from ..hw_wallet import HW_PluginBase
|
from ..hw_wallet import HW_PluginBase
|
||||||
from electrum.util import format_satoshis_plain, print_error
|
from electrum.util import format_satoshis_plain, print_error
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import hid
|
import hid
|
||||||
from btchip.btchipComm import HIDDongleHIDAPI, DongleWait
|
from btchip.btchipComm import HIDDongleHIDAPI, DongleWait
|
||||||
from btchip.btchip import btchip
|
from btchip.btchip import btchip
|
||||||
from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script
|
from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script
|
||||||
from btchip.bitcoinTransaction import bitcoinTransaction
|
from btchip.bitcoinTransaction import bitcoinTransaction
|
||||||
from btchip.btchipPersoWizard import StartBTChipPersoDialog
|
|
||||||
from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware
|
from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware
|
||||||
from btchip.btchipException import BTChipException
|
from btchip.btchipException import BTChipException
|
||||||
BTCHIP = True
|
BTCHIP = True
|
||||||
|
@ -121,8 +119,7 @@ class Ledger_Client():
|
||||||
except BTChipException, e:
|
except BTChipException, e:
|
||||||
if (e.sw == 0x6985):
|
if (e.sw == 0x6985):
|
||||||
self.dongleObject.dongle.close()
|
self.dongleObject.dongle.close()
|
||||||
dialog = StartBTChipPersoDialog()
|
self.handler.get_setup( )
|
||||||
dialog.exec_()
|
|
||||||
# Acquire the new client on the next run
|
# Acquire the new client on the next run
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
@ -172,6 +169,12 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
||||||
# device reconnects
|
# device reconnects
|
||||||
self.force_watching_only = False
|
self.force_watching_only = False
|
||||||
self.signing = False
|
self.signing = False
|
||||||
|
self.cfg = d.get('cfg', {'mode':0,'pair':''})
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
obj = Hardware_KeyStore.dump(self)
|
||||||
|
obj['cfg'] = self.cfg
|
||||||
|
return obj
|
||||||
|
|
||||||
def get_derivation(self):
|
def get_derivation(self):
|
||||||
return self.derivation
|
return self.derivation
|
||||||
|
@ -209,18 +212,19 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
||||||
info = self.get_client().signMessagePrepare(address_path, message)
|
info = self.get_client().signMessagePrepare(address_path, message)
|
||||||
pin = ""
|
pin = ""
|
||||||
if info['confirmationNeeded']:
|
if info['confirmationNeeded']:
|
||||||
# TODO : handle different confirmation types. For the time being only supports keyboard 2FA
|
pin = self.handler.get_auth( info ) # does the authenticate dialog and returns pin
|
||||||
confirmed, p, pin = self.password_dialog()
|
if not pin:
|
||||||
if not confirmed:
|
raise UserWarning(_('Cancelled by user'))
|
||||||
raise Exception('Aborted by user')
|
pin = str(pin).encode()
|
||||||
pin = pin.encode()
|
|
||||||
#self.plugin.get_client(self, True, True)
|
|
||||||
signature = self.get_client().signMessageSign(pin)
|
signature = self.get_client().signMessageSign(pin)
|
||||||
except BTChipException, e:
|
except BTChipException, e:
|
||||||
if e.sw == 0x6a80:
|
if e.sw == 0x6a80:
|
||||||
self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.")
|
self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.")
|
||||||
else:
|
else:
|
||||||
self.give_error(e, True)
|
self.give_error(e, True)
|
||||||
|
except UserWarning:
|
||||||
|
self.handler.show_error(_('Cancelled by user'))
|
||||||
|
return ''
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.give_error(e, True)
|
self.give_error(e, True)
|
||||||
finally:
|
finally:
|
||||||
|
@ -334,6 +338,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
||||||
firstTransaction = True
|
firstTransaction = True
|
||||||
inputIndex = 0
|
inputIndex = 0
|
||||||
rawTx = tx.serialize()
|
rawTx = tx.serialize()
|
||||||
|
self.get_client().enableAlternate2fa(False)
|
||||||
while inputIndex < len(inputs):
|
while inputIndex < len(inputs):
|
||||||
self.get_client().startUntrustedTransaction(firstTransaction, inputIndex,
|
self.get_client().startUntrustedTransaction(firstTransaction, inputIndex,
|
||||||
chipInputs, redeemScripts[inputIndex])
|
chipInputs, redeemScripts[inputIndex])
|
||||||
|
@ -348,44 +353,24 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
||||||
if firstTransaction:
|
if firstTransaction:
|
||||||
transactionOutput = outputData['outputData']
|
transactionOutput = outputData['outputData']
|
||||||
if outputData['confirmationNeeded']:
|
if outputData['confirmationNeeded']:
|
||||||
# TODO : handle different confirmation types. For the time being only supports keyboard 2FA
|
outputData['address'] = output
|
||||||
self.handler.clear_dialog()
|
self.handler.clear_dialog()
|
||||||
if 'keycardData' in outputData:
|
pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin
|
||||||
pin2 = ""
|
if not pin:
|
||||||
for keycardIndex in range(len(outputData['keycardData'])):
|
raise UserWarning()
|
||||||
msg = "Do not enter your device PIN here !\r\n\r\n" + \
|
if pin != 'paired':
|
||||||
"Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" + \
|
self.handler.show_message(_("Confirmed. Signing Transaction..."))
|
||||||
"For this to work, please match the character between stars of the output address using your security card\r\n\r\n" + \
|
|
||||||
"Output address : "
|
|
||||||
for index in range(len(output)):
|
|
||||||
if index == outputData['keycardData'][keycardIndex]:
|
|
||||||
msg = msg + "*" + output[index] + "*"
|
|
||||||
else:
|
|
||||||
msg = msg + output[index]
|
|
||||||
msg = msg + "\r\n"
|
|
||||||
confirmed, p, pin = self.password_dialog(msg)
|
|
||||||
if not confirmed:
|
|
||||||
raise Exception('Aborted by user')
|
|
||||||
try:
|
|
||||||
pin2 = pin2 + chr(int(pin[0], 16))
|
|
||||||
except:
|
|
||||||
raise Exception('Invalid PIN character')
|
|
||||||
pin = pin2
|
|
||||||
else:
|
|
||||||
confirmed, p, pin = self.password_dialog()
|
|
||||||
if not confirmed:
|
|
||||||
raise Exception('Aborted by user')
|
|
||||||
pin = pin.encode()
|
|
||||||
#self.plugin.get_client(self, True, True)
|
|
||||||
self.handler.show_message("Signing ...")
|
|
||||||
else:
|
else:
|
||||||
# Sign input with the provided PIN
|
# Sign input with the provided PIN
|
||||||
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex],
|
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin)
|
||||||
pin)
|
|
||||||
inputSignature[0] = 0x30 # force for 1.4.9+
|
inputSignature[0] = 0x30 # force for 1.4.9+
|
||||||
signatures.append(inputSignature)
|
signatures.append(inputSignature)
|
||||||
inputIndex = inputIndex + 1
|
inputIndex = inputIndex + 1
|
||||||
firstTransaction = False
|
if pin != 'paired':
|
||||||
|
firstTransaction = False
|
||||||
|
except UserWarning:
|
||||||
|
self.handler.show_error(_('Cancelled by user'))
|
||||||
|
return
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
self.give_error(e, True)
|
self.give_error(e, True)
|
||||||
|
@ -412,23 +397,6 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
||||||
tx.update_signatures(updatedTransaction)
|
tx.update_signatures(updatedTransaction)
|
||||||
self.signing = False
|
self.signing = False
|
||||||
|
|
||||||
def password_dialog(self, msg=None):
|
|
||||||
if not msg:
|
|
||||||
msg = _("Do not enter your device PIN here !\r\n\r\n" \
|
|
||||||
"Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" \
|
|
||||||
"For this to work, please open a text editor " \
|
|
||||||
"(on a different computer / device if you believe this computer is compromised) " \
|
|
||||||
"and put your cursor into it, unplug your Ledger Wallet and plug it back in.\r\n" \
|
|
||||||
"It should show itself to your computer as a keyboard " \
|
|
||||||
"and output the second factor along with a summary of " \
|
|
||||||
"the transaction being signed into the text-editor.\r\n\r\n" \
|
|
||||||
"Check that summary and then enter the second factor code here.\r\n" \
|
|
||||||
"Before clicking OK, re-plug the device once more (unplug it and plug it again if you read the second factor code on the same computer)")
|
|
||||||
response = self.handler.get_word(msg)
|
|
||||||
if response is None:
|
|
||||||
return False, None, None
|
|
||||||
return True, response, response
|
|
||||||
|
|
||||||
|
|
||||||
class LedgerPlugin(HW_PluginBase):
|
class LedgerPlugin(HW_PluginBase):
|
||||||
libraries_available = BTCHIP
|
libraries_available = BTCHIP
|
||||||
|
|
|
@ -9,6 +9,9 @@ from .ledger import LedgerPlugin
|
||||||
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
|
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
|
||||||
from electrum_gui.qt.util import *
|
from electrum_gui.qt.util import *
|
||||||
|
|
||||||
|
from btchip.btchipPersoWizard import StartBTChipPersoDialog
|
||||||
|
|
||||||
|
from .auth2fa import LedgerAuthDialog
|
||||||
|
|
||||||
class Plugin(LedgerPlugin, QtPluginBase):
|
class Plugin(LedgerPlugin, QtPluginBase):
|
||||||
icon_unpaired = ":icons/ledger_unpaired.png"
|
icon_unpaired = ":icons/ledger_unpaired.png"
|
||||||
|
@ -17,11 +20,14 @@ class Plugin(LedgerPlugin, QtPluginBase):
|
||||||
def create_handler(self, window):
|
def create_handler(self, window):
|
||||||
return Ledger_Handler(window)
|
return Ledger_Handler(window)
|
||||||
|
|
||||||
|
|
||||||
class Ledger_Handler(QtHandlerBase):
|
class Ledger_Handler(QtHandlerBase):
|
||||||
|
setup_signal = pyqtSignal()
|
||||||
|
auth_signal = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, win):
|
def __init__(self, win):
|
||||||
super(Ledger_Handler, self).__init__(win, 'Ledger')
|
super(Ledger_Handler, self).__init__(win, 'Ledger')
|
||||||
|
self.setup_signal.connect(self.setup_dialog)
|
||||||
|
self.auth_signal.connect(self.auth_dialog)
|
||||||
|
|
||||||
def word_dialog(self, msg):
|
def word_dialog(self, msg):
|
||||||
response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.Password)
|
response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.Password)
|
||||||
|
@ -30,3 +36,39 @@ class Ledger_Handler(QtHandlerBase):
|
||||||
else:
|
else:
|
||||||
self.word = str(response[0])
|
self.word = str(response[0])
|
||||||
self.done.set()
|
self.done.set()
|
||||||
|
|
||||||
|
def message_dialog(self, msg):
|
||||||
|
self.clear_dialog()
|
||||||
|
self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Ledger Status"))
|
||||||
|
l = QLabel(msg)
|
||||||
|
vbox = QVBoxLayout(dialog)
|
||||||
|
vbox.addWidget(l)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
def auth_dialog(self, data):
|
||||||
|
dialog = LedgerAuthDialog(self, data)
|
||||||
|
dialog.exec_()
|
||||||
|
self.word = dialog.pin
|
||||||
|
self.done.set()
|
||||||
|
|
||||||
|
def get_auth(self, data):
|
||||||
|
self.done.clear()
|
||||||
|
self.auth_signal.emit(data)
|
||||||
|
self.done.wait()
|
||||||
|
return self.word
|
||||||
|
|
||||||
|
def get_setup(self):
|
||||||
|
self.done.clear()
|
||||||
|
self.setup_signal.emit()
|
||||||
|
self.done.wait()
|
||||||
|
return
|
||||||
|
|
||||||
|
def setup_dialog(self):
|
||||||
|
dialog = StartBTChipPersoDialog()
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue