Skip to content
87 changes: 84 additions & 3 deletions electrum/gui/qml/components/wizard/WCHaveMasterKey.qml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ WizardComponent {
wizard_data['multisig_cosigner_data'][cosigner.toString()]['master_key'] = key
} else {
wizard_data['master_key'] = key
wizard_data['key_origin_derivation'] = derivation_tf.text.trim()
wizard_data['key_origin_fingerprint'] = fingerprint_tf.text.trim().toLowerCase()
}
}

Expand All @@ -41,7 +43,7 @@ WizardComponent {
}

if (!bitcoin.verifyMasterKey(key, wizard_data['wallet_type'])) {
validationtext.text = qsTr('Error: invalid master key')
validationtext.text = bitcoin.validationMessage
return false
}

Expand All @@ -60,6 +62,28 @@ WizardComponent {
return valid = true
}

function validateKeyOrigin() {
if (cosigner || wizard_data['wallet_type'] === 'multisig')
return true
if (bitcoin.masterKeyDepth(masterkey_ta.text.trim()) <= 1)
return true
var msg = bitcoin.verifyKeyOriginInfo(
masterkey_ta.text.trim(),
derivation_tf.text.trim(),
fingerprint_tf.text.trim()
)
keyOriginError.text = msg
return msg === ''
}

function revalidate() {
var keyOk = verifyMasterKey(masterkey_ta.text)
var originOk = validateKeyOrigin()
valid = keyOk && originOk
if (keyOk)
apply()
}

ColumnLayout {
width: parent.width

Expand Down Expand Up @@ -120,7 +144,7 @@ WizardComponent {
qsTr('Enter their master private key (xprv) if you want to be able to sign for them.')
].join('\n')
: [qsTr('Please enter your master private key (xprv).'),
qsTr('You can also enter a public key (xpub) here, but be aware you will then create a watch-only wallet if all cosigners are added using public keys')
qsTr('You can also enter a public key (xpub) here, but be aware you will then create a watch-only wallet if all cosigners are added using public keys.')
].join('\n')
wrapMode: Text.Wrap
}
Expand All @@ -134,7 +158,7 @@ WizardComponent {
wrapMode: TextEdit.WrapAnywhere
onTextChanged: {
if (anyActiveFocus) {
verifyMasterKey(text)
revalidate()
}
}
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
Expand Down Expand Up @@ -188,6 +212,63 @@ WizardComponent {
color: 'transparent'
}
}

ColumnLayout {
id: keyOriginSection
visible: !cosigner
&& wizard_data['wallet_type'] !== 'multisig'
&& bitcoin.masterKeyDepth(masterkey_ta.text.trim()) > 1
Layout.fillWidth: true
Layout.topMargin: constants.paddingMedium
spacing: constants.paddingSmall

Heading {
text: qsTr('Key Origin Info')
}

InfoTextArea {
Layout.fillWidth: true
text: qsTr('These fields may be required for a hardware wallet to sign the generated PSBTs.')
font.pixelSize: constants.fontSizeSmall
}

Label {
text: qsTr('Derivation path (optional)')
font.pixelSize: constants.fontSizeSmall
}
TextField {
id: derivation_tf
Layout.fillWidth: true
placeholderText: "m/84'/0'/0'"
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
onTextChanged: {
if (anyActiveFocus) revalidate()
}
}

Label {
text: qsTr('BIP32 master fingerprint (optional)')
font.pixelSize: constants.fontSizeSmall
}
TextField {
id: fingerprint_tf
Layout.fillWidth: true
placeholderText: qsTr('8 hex chars, e.g. deadbeef')
maximumLength: 8
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
onTextChanged: {
if (anyActiveFocus) revalidate()
}
}

InfoTextArea {
id: keyOriginError
Layout.fillWidth: true
visible: text !== ''
iconStyle: InfoTextArea.IconStyle.Error
font.pixelSize: constants.fontSizeSmall
}
}
}

Bitcoin {
Expand Down
34 changes: 33 additions & 1 deletion electrum/gui/qml/qebitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from electrum import mnemonic
from electrum import keystore
from electrum.i18n import _
from electrum.bip32 import is_bip32_derivation, xpub_type
from electrum.bip32 import is_bip32_derivation, xpub_type, is_xkey_consistent_with_key_origin_info, BIP32Node
from electrum.logging import get_logger
from electrum.util import get_asyncio_loop
from electrum.transaction import tx_from_any
Expand Down Expand Up @@ -98,6 +98,38 @@ def verifyMasterKey(self, key, wallet_type='standard'):
def verifyDerivationPath(self, path):
return is_bip32_derivation(path)

@pyqtSlot(str, str, str, result=str)
def verifyKeyOriginInfo(self, xpub: str, derivation: str, fingerprint: str) -> str:
derivation = derivation.strip()
fingerprint = fingerprint.strip().lower()
if not derivation and not fingerprint:
return ''
if fingerprint and (len(fingerprint) != 8
or not all(c in '0123456789abcdef' for c in fingerprint)):
return _('BIP32 fingerprint must be exactly 8 hex characters (e.g. deadbeef)')
if derivation and not is_bip32_derivation(derivation):
return _("Invalid derivation path (e.g. m/84'/0'/0')")
if xpub and keystore.is_master_key(xpub):
try:
consistent = is_xkey_consistent_with_key_origin_info(
xpub,
derivation_prefix=derivation or None,
root_fingerprint=fingerprint or None,
)
if not consistent:
return _('Derivation path is inconsistent with the master key '
'(check depth and account index)')
except Exception:
return _('Could not validate key origin info against the master key')
return ''

@pyqtSlot(str, result=int)
def masterKeyDepth(self, key: str) -> int:
try:
return BIP32Node.from_xkey(key.strip()).depth
except Exception:
return -1

@pyqtSlot(str, result=bool)
def isRawTx(self, rawtx):
try:
Expand Down
12 changes: 12 additions & 0 deletions electrum/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,18 @@ def create_storage(self, path: str, data: dict):
else:
if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
raise UserFacingException(_('Wrong key type {}').format(t1))
_der = data.get('key_origin_derivation', '').strip()
_fp = data.get('key_origin_fingerprint', '').strip().lower()
if _der or _fp:
try:
k.add_key_origin(
derivation_prefix=_der or None,
root_fingerprint=_fp or None,
)
except Exception as e:
raise UserFacingException(
_('Invalid key origin info: {}').format(e)
) from e
elif isinstance(k, keystore.Old_KeyStore):
pass
else:
Expand Down