From 3fc2127337f31576de003865023e6d486fe8c8cb Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 20 Apr 2021 08:28:16 +0200 Subject: [PATCH] import and support master private key keystores --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 10 +- .../sparrow/control/EntryCell.java | 2 +- .../control/MasterKeyDisplayDialog.java | 48 ++++ .../sparrow/control/MessageSignDialog.java | 6 +- .../control/XprvKeystoreImportPane.java | 209 ++++++++++++++++++ .../com/sparrowwallet/sparrow/io/Bip32.java | 36 +++ .../sparrowwallet/sparrow/io/Electrum.java | 6 +- .../sparrow/io/KeystoreXprvImport.java | 11 + .../com/sparrowwallet/sparrow/io/Storage.java | 2 +- .../sparrow/keystoreimport/SwController.java | 5 +- .../transaction/HeadersController.java | 2 +- .../sparrow/wallet/KeystoreController.java | 30 ++- .../sparrow/wallet/keystore.fxml | 10 +- .../sparrow/io/ColdcardMultisigTest.java | 6 +- .../sparrow/io/ElectrumTest.java | 12 +- .../sparrow/io/SpecterDesktopTest.java | 4 +- 17 files changed, 366 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/MasterKeyDisplayDialog.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/XprvKeystoreImportPane.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/Bip32.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/KeystoreXprvImport.java diff --git a/drongo b/drongo index 1aeaacaf..85e8b97a 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 1aeaacaf59484c76d5bf485dabb4a632c5230032 +Subproject commit 85e8b97a8c8d21bfbb76096285eec95d28384090 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index a735e6c4..ec489ec1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -787,7 +787,7 @@ public class AppController implements Initializable { } private void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException { - if(wallet.containsSeeds()) { + if(wallet.containsPrivateKeys()) { //Derive xpub and master fingerprint from seed, potentially with passphrase Wallet copy = wallet.copy(); for(Keystore copyKeystore : copy.getKeystores()) { @@ -823,6 +823,12 @@ public class AppController implements Initializable { keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey()); keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase()); copyKeystore.getSeed().clear(); + } else if(keystore.hasMasterPrivateExtendedKey()) { + Keystore copyKeystore = copy.getKeystores().get(i); + Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation()); + keystore.setKeyDerivation(derivedKeystore.getKeyDerivation()); + keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey()); + copyKeystore.getMasterPrivateKey().clear(); } } } @@ -966,7 +972,7 @@ public class AppController implements Initializable { WalletTabData walletTabData = (WalletTabData)tab.getUserData(); Wallet wallet = walletTabData.getWallet(); if(wallet.getKeystores().size() == 1 && - (wallet.getKeystores().get(0).hasSeed() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) { + (wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) { //Can sign and verify messageSignDialog = new MessageSignDialog(wallet); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 747d4a83..9dd2e461 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -115,7 +115,7 @@ public class EntryCell extends TreeTableCell { actionBox.getChildren().add(receiveButton); if(nodeEntry.getWallet().getKeystores().size() == 1 && - (nodeEntry.getWallet().getKeystores().get(0).hasSeed() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) { + (nodeEntry.getWallet().getKeystores().get(0).hasPrivateKey() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) { Button signMessageButton = new Button(""); Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY); signMessageGlyph.setFontSize(12); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MasterKeyDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MasterKeyDisplayDialog.java new file mode 100644 index 00000000..d13eca87 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MasterKeyDisplayDialog.java @@ -0,0 +1,48 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.sparrow.AppServices; +import javafx.application.Platform; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; + +public class MasterKeyDisplayDialog extends Dialog { + public MasterKeyDisplayDialog(Keystore decryptedKeystore) { + final DialogPane dialogPane = getDialogPane(); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + + StackPane stackPane = new StackPane(); + dialogPane.setContent(stackPane); + + AnchorPane anchorPane = new AnchorPane(); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.getStyleClass().add("edge-to-edge"); + scrollPane.setPrefHeight(200); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + anchorPane.getChildren().add(scrollPane); + scrollPane.setFitToWidth(true); + AnchorPane.setLeftAnchor(scrollPane, 0.0); + AnchorPane.setRightAnchor(scrollPane, 0.0); + + Accordion keystoreAccordion = new Accordion(); + scrollPane.setContent(keystoreAccordion); + + XprvKeystoreImportPane keystorePane = new XprvKeystoreImportPane(decryptedKeystore); + keystorePane.setAnimated(false); + keystoreAccordion.getPanes().add(keystorePane); + + stackPane.getChildren().addAll(anchorPane); + + final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + dialogPane.getButtonTypes().addAll(cancelButtonType); + + dialogPane.setPrefWidth(500); + dialogPane.setPrefHeight(260); + + Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane)); + } +} + diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java index 3cba445e..eb6c51d2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java @@ -73,7 +73,7 @@ public class MessageSignDialog extends Dialog { if(wallet.getKeystores().size() != 1) { throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required"); } - if(!wallet.getKeystores().get(0).hasSeed() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) { + if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) { throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore"); } } @@ -211,7 +211,7 @@ public class MessageSignDialog extends Dialog { return; } - if(wallet.containsSeeds()) { + if(wallet.containsPrivateKeys()) { if(wallet.isEncrypted()) { EventManager.get().post(new RequestOpenWalletsEvent()); } else { @@ -230,6 +230,7 @@ public class MessageSignDialog extends Dialog { String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null); signature.clear(); signature.appendText(signatureText); + privKey.clear(); } catch(Exception e) { log.error("Could not sign message", e); AppServices.showErrorDialog("Could not sign message", e.getMessage()); @@ -316,6 +317,7 @@ public class MessageSignDialog extends Dialog { EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done")); Wallet decryptedWallet = decryptWalletService.getValue(); signUnencryptedKeystore(decryptedWallet); + decryptedWallet.clearPrivate(); }); decryptWalletService.setOnFailed(workerStateEvent -> { EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed")); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/XprvKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/XprvKeystoreImportPane.java new file mode 100644 index 00000000..fad9bdbb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/XprvKeystoreImportPane.java @@ -0,0 +1,209 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.MnemonicException; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletModel; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.KeystoreImportEvent; +import com.sparrowwallet.sparrow.io.ImportException; +import com.sparrowwallet.sparrow.io.KeystoreXprvImport; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.controlsfx.validation.decoration.StyleClassValidationDecoration; + +import java.util.List; + +public class XprvKeystoreImportPane extends TitledDescriptionPane { + protected final Wallet wallet; + protected final KeystoreXprvImport importer; + + private Button enterXprvButton; + private SplitMenuButton importButton; + + private ExtendedKey xprv; + + public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer) { + super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); + this.wallet = wallet; + this.importer = importer; + + createImportButton(); + buttonBox.getChildren().add(importButton); + } + + public XprvKeystoreImportPane(Keystore keystore) { + super("Master Private Key", "BIP32 key", "", "image/" + WalletModel.SEED.getType() + ".png"); + this.wallet = null; + this.importer = null; + + try { + this.xprv = keystore.getExtendedMasterPrivateKey(); + } catch(MnemonicException e) { + //can't happen + } + + showHideLink.setVisible(false); + buttonBox.getChildren().clear(); + setContent(getXprvEntry(true)); + setExpanded(true); + } + + @Override + protected Control createButton() { + enterXprvButton = new Button("Enter Private Key"); + enterXprvButton.managedProperty().bind(enterXprvButton.visibleProperty()); + enterXprvButton.setOnAction(event -> { + enterXprvButton.setDisable(true); + enterXprv(); + }); + + return enterXprvButton; + } + + private void createImportButton() { + importButton = new SplitMenuButton(); + importButton.setAlignment(Pos.CENTER_RIGHT); + importButton.setText("Import Keystore"); + importButton.getStyleClass().add("default-button"); + importButton.setOnAction(event -> { + importButton.setDisable(true); + importKeystore(wallet.getScriptType().getDefaultDerivation()); + }); + String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"}; + int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length; + for(int i = 0; i < scriptAccountsLength; i++) { + MenuItem item = new MenuItem(accounts[i]); + final List derivation = wallet.getScriptType().getDefaultDerivation(i); + item.setOnAction(event -> { + importButton.setDisable(true); + importKeystore(derivation); + }); + importButton.getItems().add(item); + } + + importButton.managedProperty().bind(importButton.visibleProperty()); + importButton.setVisible(false); + } + + private void enterXprv() { + setDescription("Enter master private key"); + showHideLink.setVisible(false); + setContent(getXprvEntry(false)); + setExpanded(true); + } + + private void importKeystore(List derivation) { + importButton.setDisable(true); + try { + Keystore keystore = importer.getKeystore(derivation, xprv); + EventManager.get().post(new KeystoreImportEvent(keystore)); + } catch (ImportException e) { + String errorMessage = e.getMessage(); + if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { + errorMessage = e.getCause().getMessage(); + } + setError("Import Error", errorMessage); + importButton.setDisable(false); + } + } + + private Node getXprvEntry(boolean displayOnly) { + TextArea xprvField = new TextArea(); + xprvField.setPrefRowCount(2); + xprvField.setWrapText(true); + xprvField.getStyleClass().add("fixed-width"); + xprvField.setPromptText(ExtendedKey.Header.fromScriptType(ScriptType.P2PKH, true).getName() + (wallet != null ? "/" + ExtendedKey.Header.fromScriptType(wallet.getScriptType(), true).getName() : "") + "..."); + HBox.setHgrow(xprvField, Priority.ALWAYS); + + if(xprv != null) { + xprvField.setText(xprv.toString()); + } + if(displayOnly) { + xprvField.setEditable(false); + } + + ValidationSupport validationSupport = new ValidationSupport(); + validationSupport.registerValidator(xprvField, Validator.combine( + Validator.createEmptyValidator("xprv is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid private key", !ExtendedKey.isValid(newValue) || ExtendedKey.fromDescriptor(newValue).getKey().isPubKeyOnly()) + )); + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + + Button importXprvButton = new Button("Import"); + importXprvButton.setMinWidth(80); + importXprvButton.setDisable(true); + importXprvButton.setOnAction(event -> { + enterXprvButton.setVisible(false); + importButton.setVisible(true); + setDescription("Ready to import"); + xprv = ExtendedKey.fromDescriptor(xprvField.getText()); + setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation())); + }); + + xprvField.textProperty().addListener((observable, oldValue, newValue) -> { + importXprvButton.setDisable(newValue.isEmpty() || !ExtendedKey.isValid(newValue) || ExtendedKey.fromDescriptor(newValue).getKey().isPubKeyOnly()); + }); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.TOP_RIGHT); + contentBox.setSpacing(20); + contentBox.getChildren().add(xprvField); + if(!displayOnly) { + contentBox.getChildren().add(importXprvButton); + } + contentBox.setPadding(new Insets(10, 30, 10, 30)); + contentBox.setPrefHeight(100); + + return contentBox; + } + + private Node getDerivationEntry(List derivation) { + TextField derivationField = new TextField(); + derivationField.setPromptText("Derivation path"); + derivationField.setText(KeyDerivation.writePath(derivation)); + HBox.setHgrow(derivationField, Priority.ALWAYS); + + ValidationSupport validationSupport = new ValidationSupport(); + validationSupport.registerValidator(derivationField, Validator.combine( + Validator.createEmptyValidator("Derivation is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue)) + )); + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + + Button importDerivationButton = new Button("Import Custom Derivation Keystore"); + importDerivationButton.setDisable(true); + importDerivationButton.setOnAction(event -> { + showHideLink.setVisible(true); + setExpanded(false); + List importDerivation = KeyDerivation.parsePath(derivationField.getText()); + importKeystore(importDerivation); + }); + + derivationField.textProperty().addListener((observable, oldValue, newValue) -> { + importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation)); + importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation)); + }); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.TOP_RIGHT); + contentBox.setSpacing(20); + contentBox.getChildren().add(derivationField); + contentBox.getChildren().add(importDerivationButton); + contentBox.setPadding(new Insets(10, 30, 10, 30)); + contentBox.setPrefHeight(60); + + return contentBox; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Bip32.java b/src/main/java/com/sparrowwallet/sparrow/io/Bip32.java new file mode 100644 index 00000000..3d9197df --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/Bip32.java @@ -0,0 +1,36 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey; +import com.sparrowwallet.drongo.wallet.WalletModel; + +import java.util.List; + +public class Bip32 implements KeystoreXprvImport { + @Override + public String getName() { + return "Master Private Key (BIP32)"; + } + + @Override + public WalletModel getWalletModel() { + return WalletModel.SEED; + } + + @Override + public String getKeystoreImportDescription() { + return "Import an extended master private key (BIP 32 xprv)"; + } + + @Override + public Keystore getKeystore(List derivation, ExtendedKey xprv) throws ImportException { + try { + MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode()); + return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, derivation); + } catch(Exception e) { + throw new ImportException(e); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java index 53326883..10004f22 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java @@ -328,11 +328,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader); ek.xprv = keystore.getExtendedPrivateKey().toString(xprvHeader); ek.pw_hash_version = 1; - if(keystore.getSeed().getType() == DeterministicSeed.Type.ELECTRUM) { + if(keystore.getSeed() == null || keystore.getSeed().getType() == DeterministicSeed.Type.BIP39) { + ew.seed_type = "bip39"; + } else if(keystore.getSeed().getType() == DeterministicSeed.Type.ELECTRUM) { ek.seed = keystore.getSeed().getMnemonicString().asString(); ek.passphrase = keystore.getSeed().getPassphrase() == null ? null : keystore.getSeed().getPassphrase().asString(); - } else if(keystore.getSeed().getType() == DeterministicSeed.Type.BIP39) { - ew.seed_type = "bip39"; } ew.use_encryption = false; } else if(keystore.getSource() == KeystoreSource.SW_WATCH) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoreXprvImport.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreXprvImport.java new file mode 100644 index 00000000..eb6006f9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreXprvImport.java @@ -0,0 +1,11 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.wallet.Keystore; + +import java.util.List; + +public interface KeystoreXprvImport extends KeystoreImport { + Keystore getKeystore(List derivation, ExtendedKey xprv) throws ImportException; +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index d59f96ba..15242f00 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -527,7 +527,7 @@ public class Storage { @Override public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) { JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore); - if(keystore.hasSeed()) { + if(keystore.hasPrivateKey()) { jsonObject.remove("extendedPublicKey"); jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint"); } diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java index 253aaa61..b7ed6b66 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.keystoreimport; import com.sparrowwallet.sparrow.control.FileKeystoreImportPane; import com.sparrowwallet.sparrow.control.MnemonicKeystoreImportPane; import com.sparrowwallet.sparrow.control.TitledDescriptionPane; +import com.sparrowwallet.sparrow.control.XprvKeystoreImportPane; import com.sparrowwallet.sparrow.io.*; import javafx.fxml.FXML; import javafx.scene.control.Accordion; @@ -14,7 +15,7 @@ public class SwController extends KeystoreImportDetailController { private Accordion importAccordion; public void initializeView() { - List importers = List.of(new Bip39(), new Electrum()); + List importers = List.of(new Bip39(), new Electrum(), new Bip32()); for(KeystoreImport importer : importers) { TitledDescriptionPane importPane = null; @@ -23,6 +24,8 @@ public class SwController extends KeystoreImportDetailController { importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer); } else if(importer instanceof KeystoreMnemonicImport) { importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer); + } else if(importer instanceof KeystoreXprvImport) { + importPane = new XprvKeystoreImportPane(getMasterController().getWallet(), (KeystoreXprvImport)importer); } else { throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index b41083c8..475304aa 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -698,7 +698,7 @@ public class HeadersController extends TransactionFormController implements Init } private void signSoftwareKeystores() { - if(headersForm.getSigningWallet().getKeystores().stream().noneMatch(Keystore::hasSeed)) { + if(headersForm.getSigningWallet().getKeystores().stream().noneMatch(Keystore::hasPrivateKey)) { return; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index 1b0ab3c6..7e0667a9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -7,10 +7,7 @@ import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.control.QRDisplayDialog; -import com.sparrowwallet.sparrow.control.QRScanDialog; -import com.sparrowwallet.sparrow.control.SeedDisplayDialog; -import com.sparrowwallet.sparrow.control.WalletPasswordDialog; +import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.StorageEvent; import com.sparrowwallet.sparrow.event.TimedEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; @@ -57,6 +54,9 @@ public class KeystoreController extends WalletFormController implements Initiali @FXML private Button viewSeedButton; + @FXML + private Button viewKeyButton; + @FXML private Button importButton; @@ -106,6 +106,7 @@ public class KeystoreController extends WalletFormController implements Initiali } viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty()); + viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty()); scanXpubQR.managedProperty().bind(scanXpubQR.visibleProperty()); displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty()); displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not()); @@ -246,7 +247,8 @@ public class KeystoreController extends WalletFormController implements Initiali private void updateType() { type.setText(getTypeLabel(keystore)); type.setGraphic(getTypeIcon(keystore)); - viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED); + viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed()); + viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey()); importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace..."); importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source")); @@ -317,6 +319,7 @@ public class KeystoreController extends WalletFormController implements Initiali keystore.setLabel(importedKeystore.getLabel()); keystore.setKeyDerivation(importedKeystore.getKeyDerivation()); keystore.setExtendedPublicKey(importedKeystore.getExtendedPublicKey()); + keystore.setMasterPrivateExtendedKey(importedKeystore.getMasterPrivateExtendedKey()); keystore.setSeed(importedKeystore.getSeed()); updateType(); @@ -333,7 +336,7 @@ public class KeystoreController extends WalletFormController implements Initiali } } - public void showSeed(ActionEvent event) { + public void showPrivate(ActionEvent event) { int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore); Wallet copy = getWalletForm().getWallet().copy(); @@ -345,7 +348,7 @@ public class KeystoreController extends WalletFormController implements Initiali decryptWalletService.setOnSucceeded(workerStateEvent -> { EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.END, "Done")); Wallet decryptedWallet = decryptWalletService.getValue(); - showSeed(decryptedWallet.getKeystores().get(keystoreIndex)); + showPrivate(decryptedWallet.getKeystores().get(keystoreIndex)); }); decryptWalletService.setOnFailed(workerStateEvent -> { EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.END, "Failed")); @@ -355,13 +358,18 @@ public class KeystoreController extends WalletFormController implements Initiali decryptWalletService.start(); } } else { - showSeed(keystore); + showPrivate(keystore); } } - private void showSeed(Keystore keystore) { - SeedDisplayDialog dlg = new SeedDisplayDialog(keystore); - dlg.showAndWait(); + private void showPrivate(Keystore keystore) { + if(keystore.hasSeed()) { + SeedDisplayDialog dlg = new SeedDisplayDialog(keystore); + dlg.showAndWait(); + } else if(keystore.hasMasterPrivateExtendedKey()) { + MasterKeyDisplayDialog dlg = new MasterKeyDisplayDialog(keystore); + dlg.showAndWait(); + } } public void scanXpubQR(ActionEvent event) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml index b8982d4e..a5c89ccf 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml @@ -22,7 +22,7 @@