diff --git a/drongo b/drongo index 04576bdd..eb07a7ff 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 04576bddff218f284e0cf925a1f790011203eec4 +Subproject commit eb07a7ffa3c46cda83ac951d3b1ebd529460554c diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java index 1b7ec051..5796b651 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java @@ -2,13 +2,8 @@ package com.sparrowwallet.sparrow.control; import com.google.gson.JsonParseException; import com.sparrowwallet.drongo.crypto.InvalidPasswordException; -import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.io.FileImport; import com.sparrowwallet.sparrow.io.ImportException; -import com.sparrowwallet.sparrow.io.KeystoreFileImport; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -21,6 +16,8 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; import org.controlsfx.control.textfield.CustomPasswordField; import org.controlsfx.control.textfield.TextFields; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.File; @@ -28,6 +25,8 @@ import java.io.FileInputStream; import java.io.InputStream; public abstract class FileImportPane extends TitledDescriptionPane { + private static final Logger log = LoggerFactory.getLogger(FileImportPane.class); + private final FileImport importer; private Button importButton; private final SimpleStringProperty password = new SimpleStringProperty(""); @@ -77,6 +76,7 @@ public abstract class FileImportPane extends TitledDescriptionPane { importFile(file.getName(), inputStream, password); } } catch (Exception e) { + log.error("Error importing file", e); String errorMessage = e.getMessage(); if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { errorMessage = e.getCause().getMessage(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java index 6f1d63fe..a570c45a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java @@ -55,6 +55,16 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { buttonBox.getChildren().add(importButton); } + public MnemonicKeystoreImportPane(Keystore keystore) { + super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED + ".png"); + this.wallet = null; + this.importer = null; + showHideLink.setVisible(false); + buttonBox.getChildren().clear(); + + showWordList(keystore.getSeed()); + } + @Override protected Control createButton() { createEnterMnemonicButton(); @@ -108,11 +118,11 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { generatedMnemonicCode = null; setDescription("Enter mnemonic word list"); showHideLink.setVisible(false); - setContent(getMnemonicWordsEntry(numWords)); + setContent(getMnemonicWordsEntry(numWords, false)); setExpanded(true); } - private Node getMnemonicWordsEntry(int numWords) { + private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) { VBox vBox = new VBox(); vBox.setSpacing(10); @@ -136,52 +146,54 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { vBox.getChildren().add(wordsPane); - PassphraseEntry passphraseEntry = new PassphraseEntry(); - passphraseEntry.setPadding(new Insets(0, 32, 0, 10)); - vBox.getChildren().add(passphraseEntry); + if(!displayWordsOnly) { + PassphraseEntry passphraseEntry = new PassphraseEntry(); + passphraseEntry.setPadding(new Insets(0, 32, 0, 10)); + vBox.getChildren().add(passphraseEntry); - AnchorPane buttonPane = new AnchorPane(); - buttonPane.setPadding(new Insets(0, 32, 0, 10)); + AnchorPane buttonPane = new AnchorPane(); + buttonPane.setPadding(new Insets(0, 32, 0, 10)); - Button generateButton = new Button("Generate New"); - generateButton.setOnAction(event -> { - generateNew(); - }); - buttonPane.getChildren().add(generateButton); - AnchorPane.setLeftAnchor(generateButton, 0.0); + Button generateButton = new Button("Generate New"); + generateButton.setOnAction(event -> { + generateNew(); + }); + buttonPane.getChildren().add(generateButton); + AnchorPane.setLeftAnchor(generateButton, 0.0); - confirmButton = new Button("Confirm Backup"); - confirmButton.setOnAction(event -> { - confirmBackup(); - }); - confirmButton.managedProperty().bind(confirmButton.visibleProperty()); - confirmButton.setVisible(false); - confirmButton.setDefaultButton(true); - buttonPane.getChildren().add(confirmButton); - AnchorPane.setRightAnchor(confirmButton, 0.0); + confirmButton = new Button("Confirm Backup"); + confirmButton.setOnAction(event -> { + confirmBackup(); + }); + confirmButton.managedProperty().bind(confirmButton.visibleProperty()); + confirmButton.setVisible(false); + confirmButton.setDefaultButton(true); + buttonPane.getChildren().add(confirmButton); + AnchorPane.setRightAnchor(confirmButton, 0.0); - verifyButton = new Button("Verify"); - verifyButton.setDisable(true); - verifyButton.setDefaultButton(true); - verifyButton.setOnAction(event -> { - prepareImport(); - }); - verifyButton.managedProperty().bind(verifyButton.visibleProperty()); + verifyButton = new Button("Verify"); + verifyButton.setDisable(true); + verifyButton.setDefaultButton(true); + verifyButton.setOnAction(event -> { + prepareImport(); + }); + verifyButton.managedProperty().bind(verifyButton.visibleProperty()); - wordEntriesProperty.addListener((ListChangeListener) c -> { - for(String word : wordEntryList) { - if(!WordEntry.isValid(word)) { - verifyButton.setDisable(true); - return; + wordEntriesProperty.addListener((ListChangeListener) c -> { + for(String word : wordEntryList) { + if(!WordEntry.isValid(word)) { + verifyButton.setDisable(true); + return; + } } - } - verifyButton.setDisable(false); - }); - buttonPane.getChildren().add(verifyButton); - AnchorPane.setRightAnchor(verifyButton, 0.0); + verifyButton.setDisable(false); + }); + buttonPane.getChildren().add(verifyButton); + AnchorPane.setRightAnchor(verifyButton, 0.0); - vBox.getChildren().add(buttonPane); + vBox.getChildren().add(buttonPane); + } return vBox; } @@ -212,7 +224,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { private void confirmBackup() { setDescription("Confirm backup by re-entering words"); showHideLink.setVisible(false); - setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size())); + setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false)); setExpanded(true); } @@ -368,4 +380,16 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { return contentBox; } + + private void showWordList(DeterministicSeed seed) { + List words = seed.getMnemonicCode(); + setContent(getMnemonicWordsEntry(words.size(), true)); + setExpanded(true); + + for (int i = 0; i < wordsPane.getChildren().size(); i++) { + WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i); + wordEntry.getEditor().setText(words.get(i)); + wordEntry.getEditor().setEditable(false); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java new file mode 100644 index 00000000..8ca81a56 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java @@ -0,0 +1,47 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.sparrow.AppController; +import javafx.application.Platform; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; + +public class SeedDisplayDialog extends Dialog { + public SeedDisplayDialog(Keystore decryptedKeystore) { + final DialogPane dialogPane = getDialogPane(); + dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm()); + + int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3; + int height = lines * 40; + + StackPane stackPane = new StackPane(); + dialogPane.setContent(stackPane); + + AnchorPane anchorPane = new AnchorPane(); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setPrefHeight(74 + height); + 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); + + MnemonicKeystoreImportPane keystorePane = new MnemonicKeystoreImportPane(decryptedKeystore); + 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(150 + height); + + Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane)); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java index 51a38988..f37189b8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java @@ -163,6 +163,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport } private String decrypt(String encrypted, String password) { + if(encrypted == null) { + return null; + } + KeyDeriver keyDeriver = new DoubleSha256KeyDeriver(); Key key = keyDeriver.deriveKey(password); byte[] encryptedBytes = Base64.getDecoder().decode(encrypted); diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java index beb97f93..fd928339 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/KeystoreImportDialog.java @@ -15,6 +15,7 @@ import javafx.scene.control.DialogPane; import org.controlsfx.tools.Borders; import java.io.IOException; +import java.util.List; public class KeystoreImportDialog extends Dialog { private final KeystoreImportController keystoreImportController; @@ -46,6 +47,10 @@ public class KeystoreImportDialog extends Dialog { } } + public static List getSupportedSources() { + return List.of(KeystoreSource.HW_USB, KeystoreSource.HW_AIRGAPPED, KeystoreSource.SW_SEED); + } + @Subscribe public void keystoreImported(KeystoreImportEvent event) { this.keystore = event.getKeystore(); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 709a0063..2304c796 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -752,7 +752,7 @@ public class HeadersController extends TransactionFormController implements Init @Subscribe public void openWallets(OpenWalletsEvent event) { - if(headersForm.getPsbt() != null) { + if(headersForm.getPsbt() != null && headersForm.getBlockTransaction() == null) { List availableWallets = event.getWallets().stream().filter(wallet -> wallet.canSign(headersForm.getPsbt())).collect(Collectors.toList()); Map availableWalletsMap = new LinkedHashMap<>(event.getWalletsMap()); availableWalletsMap.keySet().retainAll(availableWallets); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index c2f5453e..42586645 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -3,10 +3,19 @@ package com.sparrowwallet.sparrow.wallet; import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.control.SeedDisplayDialog; +import com.sparrowwallet.sparrow.control.WalletPasswordDialog; +import com.sparrowwallet.sparrow.event.StorageEvent; +import com.sparrowwallet.sparrow.event.TimedEvent; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog; import com.sparrowwallet.sparrow.event.SettingsChangedEvent; import javafx.application.Platform; @@ -14,7 +23,9 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; +import org.controlsfx.glyphfont.Glyph; import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.Validator; @@ -37,6 +48,9 @@ public class KeystoreController extends WalletFormController implements Initiali @FXML private Label type; + @FXML + private Button importButton; + @FXML private TextField label; @@ -156,6 +170,15 @@ public class KeystoreController extends WalletFormController implements Initiali private void updateType() { type.setText(getTypeLabel(keystore)); + if(keystore.getSource() == KeystoreSource.SW_SEED) { + Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EYE); + searchGlyph.setFontSize(12); + type.setGraphic(searchGlyph); + } else { + type.setGraphic(null); + } + + importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Edit..."); boolean editable = (keystore.getSource() == KeystoreSource.SW_WATCH); fingerprint.setEditable(editable); @@ -178,7 +201,12 @@ public class KeystoreController extends WalletFormController implements Initiali } public void importKeystore(ActionEvent event) { - launchImportDialog(KeystoreSource.HW_USB); + KeystoreSource initialSource = keystore.getSource(); + if(initialSource == null || !KeystoreImportDialog.getSupportedSources().contains(initialSource)) { + initialSource = KeystoreImportDialog.getSupportedSources().get(0); + } + + launchImportDialog(initialSource); } private void launchImportDialog(KeystoreSource initialSource) { @@ -204,6 +232,37 @@ public class KeystoreController extends WalletFormController implements Initiali } } + public void showSeed(MouseEvent event) { + int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore); + Wallet copy = getWalletForm().getWallet().copy(); + + if(copy.isEncrypted()) { + WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD); + Optional password = dlg.showAndWait(); + if(password.isPresent()) { + Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get()); + decryptWalletService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.END, "Done")); + Wallet decryptedWallet = decryptWalletService.getValue(); + showSeed(decryptedWallet.getKeystores().get(keystoreIndex)); + }); + decryptWalletService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.END, "Failed")); + AppController.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage()); + }); + EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.START, "Decrypting wallet...")); + decryptWalletService.start(); + } + } else { + showSeed(keystore); + } + } + + private void showSeed(Keystore keystore) { + SeedDisplayDialog dlg = new SeedDisplayDialog(keystore); + dlg.showAndWait(); + } + @Subscribe public void update(SettingsChangedEvent event) { if(walletForm.getWallet().equals(event.getWallet()) && event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml index 8b1494dd..2a143ddb 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml @@ -20,9 +20,9 @@
-