From 7e1606fe864e284850f44db70321933f79eb65a2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 10 Mar 2022 20:35:14 +0100 Subject: [PATCH] validate seeds for Electrum, BIP39, SLIP39 seeds and perform create wallet in from seed scenario Currently only Electrum seeds are considered valid. For BIP39 additional dialog is needed. For SLIP39 multiple mnemonics need to be supported to generate a seed --- .../gui/qml/components/NewWalletWizard.qml | 2 + electrum/gui/qml/components/Wallets.qml | 2 + .../gui/qml/components/WizardComponents.qml | 85 +++++++++++++++---- electrum/gui/qml/qebitcoin.py | 82 +++++++++++++++--- electrum/gui/qml/qedaemon.py | 5 +- electrum/gui/qml/qewalletdb.py | 6 +- 6 files changed, 150 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qml/components/NewWalletWizard.qml b/electrum/gui/qml/components/NewWalletWizard.qml index 8a0c13c21..37365ba8c 100644 --- a/electrum/gui/qml/components/NewWalletWizard.qml +++ b/electrum/gui/qml/components/NewWalletWizard.qml @@ -11,6 +11,8 @@ Wizard { signal walletCreated + property alias path: walletdb.path + enter: null // disable transition // State transition functions. These functions are called when the 'Next' diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 9877754e4..11c627ec0 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -107,6 +107,8 @@ Pane { dialog.open() dialog.walletCreated.connect(function() { Daemon.availableWallets.reload() + // and load the new wallet + Daemon.load_wallet(dialog.path, dialog.wizard_data['password']) }) } } diff --git a/electrum/gui/qml/components/WizardComponents.qml b/electrum/gui/qml/components/WizardComponents.qml index c8dd3df93..7110820ab 100644 --- a/electrum/gui/qml/components/WizardComponents.qml +++ b/electrum/gui/qml/components/WizardComponents.qml @@ -123,18 +123,18 @@ Item { function setWarningText(numwords) { var t = [ - "

", - qsTr("Please save these %1 words on paper (order is important). ").arg(numwords), - qsTr("This seed will allow you to recover your wallet in case of computer failure."), - "

", - "" + qsTr("WARNING") + ":", - "" + '

', + qsTr('Please save these %1 words on paper (order is important).').arg(numwords), + qsTr('This seed will allow you to recover your wallet in case of computer failure.'), + '

', + '' + qsTr('WARNING') + ':', + '' ] - warningtext.text = t.join("") + warningtext.text = t.join(' ') } Flickable { @@ -187,7 +187,7 @@ Item { id: bitcoin onGeneratedSeedChanged: { seedtext.text = generated_seed - setWarningText(generated_seed.split(" ").length) + setWarningText(generated_seed.split(' ').length) } } } @@ -195,16 +195,16 @@ Item { property Component haveseed: Component { WizardComponent { + id: root valid: false onAccept: { wizard_data['seed'] = seedtext.text + wizard_data['seed_type'] = bitcoin.seed_type wizard_data['seed_extend'] = extendcb.checked wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' - wizard_data['seed_bip39'] = bip39cb.checked - } - - function checkValid() { + wizard_data['seed_bip39'] = seed_type.getTypeCode() == 'BIP39' + wizard_data['seed_slip39'] = seed_type.getTypeCode() == 'SLIP39' } function setSeedTypeHelpText() { @@ -230,6 +230,10 @@ Item { infotext.text = t[seed_type.currentText] } + function checkValid() { + bitcoin.verify_seed(seedtext.text, seed_type.getTypeCode() == 'BIP39', seed_type.getTypeCode() == 'SLIP39') + } + Flickable { anchors.fill: parent contentHeight: mainLayout.height @@ -243,11 +247,18 @@ Item { Label { text: qsTr('Seed Type') + Layout.fillWidth: true } ComboBox { id: seed_type model: ['Electrum', 'BIP39', 'SLIP39'] - onActivated: setSeedTypeHelpText() + onActivated: { + setSeedTypeHelpText() + checkValid() + } + function getTypeCode() { + return currentText + } } InfoTextArea { id: infotext @@ -263,9 +274,36 @@ Item { Layout.fillWidth: true Layout.columnSpan: 2 onTextChanged: { - checkValid() + validationTimer.restart() + } + + Rectangle { + anchors.fill: contentText + color: 'green' + border.color: Material.accentColor + radius: 2 + } + Label { + id: contentText + anchors.right: parent.right + anchors.bottom: parent.bottom + leftPadding: text != '' ? 16 : 0 + rightPadding: text != '' ? 16 : 0 + font.bold: false + font.pixelSize: 13 } } + TextArea { + id: validationtext + visible: text != '' + Layout.fillWidth: true + readOnly: true + wrapMode: TextInput.WordWrap + background: Rectangle { + color: 'transparent' + } + } + CheckBox { id: extendcb Layout.columnSpan: 2 @@ -284,7 +322,18 @@ Item { Bitcoin { id: bitcoin + onSeedTypeChanged: contentText.text = bitcoin.seed_type + onSeedValidChanged: root.valid = bitcoin.seed_valid + onValidationMessageChanged: validationtext.text = bitcoin.validation_message } + + Timer { + id: validationTimer + interval: 500 + repeat: false + onTriggered: checkValid() + } + Component.onCompleted: { setSeedTypeHelpText() } diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index d4f602235..16ed23a05 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -3,6 +3,8 @@ import asyncio from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.logging import get_logger +from electrum.keystore import bip39_is_checksum_valid +from electrum.slip39 import decode_mnemonic, Slip39Error from electrum import mnemonic class QEBitcoin(QObject): @@ -18,10 +20,28 @@ class QEBitcoin(QObject): seedValidChanged = pyqtSignal() seedValid = False + seedTypeChanged = pyqtSignal() + seedType = '' + + validationMessageChanged = pyqtSignal() + validationMessage = '' + @pyqtProperty('QString', notify=generatedSeedChanged) def generated_seed(self): return self.generatedSeed + @pyqtProperty(bool, notify=seedValidChanged) + def seed_valid(self): + return self.seedValid + + @pyqtProperty('QString', notify=seedTypeChanged) + def seed_type(self): + return self.seedType + + @pyqtProperty('QString', notify=validationMessageChanged) + def validation_message(self): + return self.validationMessage + @pyqtSlot() @pyqtSlot(str) @pyqtSlot(str,str) @@ -36,17 +56,55 @@ class QEBitcoin(QObject): loop = asyncio.get_event_loop() asyncio.run_coroutine_threadsafe(co_gen_seed(seed_type, language), loop) - @pyqtProperty(bool, notify=seedValidChanged) - def seed_valid(self): - return self.seedValid - @pyqtSlot(str) - @pyqtSlot(str,str) - @pyqtSlot(str,str,str) - @pyqtSlot(str,str,str,str) - def verify_seed(self, seed, bip39=False, seed_type='segwit', language='en'): - self._logger.debug('verify seed of type ' + str(seed_type)) - #TODO - #self._logger.debug('seed verified') - #self.seedValidChanged.emit() + @pyqtSlot(str,bool,bool) + @pyqtSlot(str,bool,bool,str,str,str) + def verify_seed(self, seed, bip39=False, slip39=False, wallet_type='standard', language='en'): + self._logger.debug('bip39 ' + str(bip39)) + self._logger.debug('slip39 ' + str(slip39)) + + seed_type = '' + seed_valid = False + validation_message = '' + + if not (bip39 or slip39): + seed_type = mnemonic.seed_type(seed) + if seed_type != '': + seed_valid = True + elif bip39: + is_checksum, is_wordlist = bip39_is_checksum_valid(seed) + status = ('checksum: ' + ('ok' if is_checksum else 'failed')) if is_wordlist else 'unknown wordlist' + validation_message = 'BIP39 (%s)' % status + + if is_checksum: + seed_type = 'bip39' + seed_valid = True + seed_valid = False # for now + + elif slip39: # TODO: incomplete impl, this code only validates a single share. + try: + share = decode_mnemonic(seed) + seed_type = 'slip39' + validation_message = 'SLIP39: share #%d in %dof%d scheme' % (share.group_index, share.group_threshold, share.group_count) + except Slip39Error as e: + validation_message = 'SLIP39: %s' % str(e) + seed_valid = False # for now + + # cosigning seed + if wallet_type != 'standard' and seed_type not in ['standard', 'segwit']: + seed_type = '' + seed_valid = False + + self.seedType = seed_type + self.seedTypeChanged.emit() + + if self.validationMessage != validation_message: + self.validationMessage = validation_message + self.validationMessageChanged.emit() + + if self.seedValid != seed_valid: + self.seedValid = seed_valid + self.seedValidChanged.emit() + + self._logger.debug('seed verified: ' + str(seed_valid)) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index e98e7dd90..8feeab858 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -6,7 +6,7 @@ from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex from electrum.util import register_callback, get_new_wallet_name from electrum.logging import get_logger from electrum.wallet import Wallet, Abstract_Wallet -from electrum.storage import WalletStorage +from electrum.storage import WalletStorage, StorageReadWriteError from .qewallet import QEWallet @@ -106,6 +106,9 @@ class QEDaemon(QObject): self._logger.debug('load wallet ' + str(self._path)) try: storage = WalletStorage(self._path) + if not storage.file_exists(): + self.couldNotOpenFile.emit() + return except StorageReadWriteError as e: self.couldNotOpenFile.emit() return diff --git a/electrum/gui/qml/qewalletdb.py b/electrum/gui/qml/qewalletdb.py index fe0d3bb16..7a638860f 100644 --- a/electrum/gui/qml/qewalletdb.py +++ b/electrum/gui/qml/qewalletdb.py @@ -59,8 +59,8 @@ class QEWalletDB(QObject): if wallet_path == self._path: return + self._logger.info('setting path: ' + wallet_path) self.reset() - self._logger.warning('path: ' + wallet_path) self._path = wallet_path self.load_storage() @@ -229,6 +229,10 @@ class QEWalletDB(QObject): db.load_plugins() db.write(storage) + # minimally populate self after create + self._password = data['password'] + self.path = path + self.createSuccess.emit() except Exception as e: self._logger.error(str(e))