auth2fa.py (7138B)
1 import copy 2 from typing import TYPE_CHECKING 3 4 from PyQt5.QtWidgets import (QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel, 5 QWidget, QHBoxLayout, QComboBox) 6 7 from btchip.btchip import BTChipException 8 9 from electrum.gui.qt.util import PasswordLineEdit 10 11 from electrum.i18n import _ 12 from electrum import constants, bitcoin 13 from electrum.logging import get_logger 14 15 if TYPE_CHECKING: 16 from .ledger import Ledger_Client 17 18 19 _logger = get_logger(__name__) 20 21 22 DEBUG = False 23 24 helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.<br><br>" \ 25 "For best security you should unplug your device, open a text editor on another computer, " \ 26 "put your cursor into it, and plug your device into that computer. " \ 27 "It will output a summary of the transaction being signed and a one-time PIN.<br><br>" \ 28 "Verify the transaction summary and type the PIN code here.<br><br>" \ 29 "Before pressing enter, plug the device back into this computer.<br>" ), 30 _("Verify the address below.<br>Type the character from your security card corresponding to the <u><b>BOLD</b></u> character."), 31 ] 32 33 class LedgerAuthDialog(QDialog): 34 def __init__(self, handler, data, *, client: 'Ledger_Client'): 35 '''Ask user for 2nd factor authentication. Support text and security card methods. 36 Use last method from settings, but support downgrade. 37 ''' 38 QDialog.__init__(self, handler.top_level_window()) 39 self.handler = handler 40 self.txdata = data 41 self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else '' 42 self.setMinimumWidth(650) 43 self.setWindowTitle(_("Ledger Wallet Authentication")) 44 self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg) 45 self.dongle = client.dongleObject.dongle 46 self.pin = '' 47 48 self.devmode = self.getDevice2FAMode() 49 if self.devmode == 0x11 or self.txdata['confirmationType'] == 1: 50 self.cfg['mode'] = 0 51 52 vbox = QVBoxLayout() 53 self.setLayout(vbox) 54 55 def on_change_mode(idx): 56 self.cfg['mode'] = 0 if self.devmode == 0x11 else idx if idx > 0 else 1 57 if self.cfg['mode'] > 0: 58 self.handler.win.wallet.get_keystore().cfg = self.cfg 59 self.handler.win.wallet.save_keystore() 60 self.update_dlg() 61 def return_pin(): 62 self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text() 63 if self.cfg['mode'] == 1: 64 self.pin = ''.join(chr(int(str(i),16)) for i in self.pin) 65 self.accept() 66 67 self.modebox = QWidget() 68 modelayout = QHBoxLayout() 69 self.modebox.setLayout(modelayout) 70 modelayout.addWidget(QLabel(_("Method:"))) 71 self.modes = QComboBox() 72 modelayout.addWidget(self.modes, 2) 73 modelayout.addStretch(1) 74 self.modebox.setMaximumHeight(50) 75 vbox.addWidget(self.modebox) 76 77 self.populate_modes() 78 self.modes.currentIndexChanged.connect(on_change_mode) 79 80 self.helpmsg = QTextEdit() 81 self.helpmsg.setStyleSheet("QTextEdit { color:black; background-color: lightgray; }") 82 self.helpmsg.setReadOnly(True) 83 vbox.addWidget(self.helpmsg) 84 85 self.pinbox = QWidget() 86 pinlayout = QHBoxLayout() 87 self.pinbox.setLayout(pinlayout) 88 self.pintxt = PasswordLineEdit() 89 self.pintxt.setMaxLength(4) 90 self.pintxt.returnPressed.connect(return_pin) 91 pinlayout.addWidget(QLabel(_("Enter PIN:"))) 92 pinlayout.addWidget(self.pintxt) 93 pinlayout.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) 94 pinlayout.addStretch(1) 95 self.pinbox.setVisible(self.cfg['mode'] == 0) 96 vbox.addWidget(self.pinbox) 97 98 self.cardbox = QWidget() 99 card = QVBoxLayout() 100 self.cardbox.setLayout(card) 101 self.addrtext = QTextEdit() 102 self.addrtext.setStyleSheet(''' 103 QTextEdit { 104 color:blue; background-color:lightgray; padding:15px 10px; border:none; 105 font-size:20pt; font-family: "Courier New", monospace; } 106 ''') 107 self.addrtext.setReadOnly(True) 108 self.addrtext.setMaximumHeight(130) 109 card.addWidget(self.addrtext) 110 111 def pin_changed(s): 112 if len(s) < len(self.idxs): 113 i = self.idxs[len(s)] 114 addr = self.txdata['address'] 115 if not constants.net.TESTNET: 116 text = addr[:i] + '<u><b>' + addr[i:i+1] + '</u></b>' + addr[i+1:] 117 else: 118 # pin needs to be created from mainnet address 119 addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), net=constants.BitcoinMainnet) 120 addr_mainnet = addr_mainnet[:i] + '<u><b>' + addr_mainnet[i:i+1] + '</u></b>' + addr_mainnet[i+1:] 121 text = str(addr) + '\n' + str(addr_mainnet) 122 self.addrtext.setHtml(str(text)) 123 else: 124 self.addrtext.setHtml(_("Press Enter")) 125 126 pin_changed('') 127 cardpin = QHBoxLayout() 128 cardpin.addWidget(QLabel(_("Enter PIN:"))) 129 self.cardtxt = PasswordLineEdit() 130 self.cardtxt.setMaxLength(len(self.idxs)) 131 self.cardtxt.textChanged.connect(pin_changed) 132 self.cardtxt.returnPressed.connect(return_pin) 133 cardpin.addWidget(self.cardtxt) 134 cardpin.addWidget(QLabel(_("NOT DEVICE PIN - see above"))) 135 cardpin.addStretch(1) 136 card.addLayout(cardpin) 137 self.cardbox.setVisible(self.cfg['mode'] == 1) 138 vbox.addWidget(self.cardbox) 139 140 self.update_dlg() 141 142 def populate_modes(self): 143 self.modes.blockSignals(True) 144 self.modes.clear() 145 self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled")) 146 if self.txdata['confirmationType'] > 1: 147 self.modes.addItem(_("Security Card Challenge")) 148 self.modes.blockSignals(False) 149 150 def update_dlg(self): 151 self.modes.setCurrentIndex(self.cfg['mode']) 152 self.modebox.setVisible(True) 153 self.helpmsg.setText(helpTxt[self.cfg['mode']]) 154 self.helpmsg.setMinimumHeight(180 if self.txdata['confirmationType'] == 1 else 100) 155 self.helpmsg.setVisible(True) 156 self.pinbox.setVisible(self.cfg['mode'] == 0) 157 self.cardbox.setVisible(self.cfg['mode'] == 1) 158 self.pintxt.setFocus(True) if self.cfg['mode'] == 0 else self.cardtxt.setFocus(True) 159 self.setMaximumHeight(400) 160 161 def getDevice2FAMode(self): 162 apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode 163 try: 164 mode = self.dongle.exchange( bytearray(apdu) ) 165 return mode 166 except BTChipException as e: 167 _logger.debug('Device getMode Failed') 168 return 0x11