show seed words dialog, fix electrum import

This commit is contained in:
Craig Raw 2020-08-05 16:55:18 +02:00
parent ce60fe3dc6
commit 53478d9b22
9 changed files with 190 additions and 51 deletions

2
drongo

@ -1 +1 @@
Subproject commit 04576bddff218f284e0cf925a1f790011203eec4 Subproject commit eb07a7ffa3c46cda83ac951d3b1ebd529460554c

View file

@ -2,13 +2,8 @@ package com.sparrowwallet.sparrow.control;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException; 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.FileImport;
import com.sparrowwallet.sparrow.io.ImportException; import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -21,6 +16,8 @@ import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.controlsfx.control.textfield.CustomPasswordField; import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields; import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
@ -28,6 +25,8 @@ import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
public abstract class FileImportPane extends TitledDescriptionPane { public abstract class FileImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(FileImportPane.class);
private final FileImport importer; private final FileImport importer;
private Button importButton; private Button importButton;
private final SimpleStringProperty password = new SimpleStringProperty(""); private final SimpleStringProperty password = new SimpleStringProperty("");
@ -77,6 +76,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
importFile(file.getName(), inputStream, password); importFile(file.getName(), inputStream, password);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage(); String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage(); errorMessage = e.getCause().getMessage();

View file

@ -55,6 +55,16 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
buttonBox.getChildren().add(importButton); 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 @Override
protected Control createButton() { protected Control createButton() {
createEnterMnemonicButton(); createEnterMnemonicButton();
@ -108,11 +118,11 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
generatedMnemonicCode = null; generatedMnemonicCode = null;
setDescription("Enter mnemonic word list"); setDescription("Enter mnemonic word list");
showHideLink.setVisible(false); showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords)); setContent(getMnemonicWordsEntry(numWords, false));
setExpanded(true); setExpanded(true);
} }
private Node getMnemonicWordsEntry(int numWords) { private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) {
VBox vBox = new VBox(); VBox vBox = new VBox();
vBox.setSpacing(10); vBox.setSpacing(10);
@ -136,6 +146,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
vBox.getChildren().add(wordsPane); vBox.getChildren().add(wordsPane);
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry(); PassphraseEntry passphraseEntry = new PassphraseEntry();
passphraseEntry.setPadding(new Insets(0, 32, 0, 10)); passphraseEntry.setPadding(new Insets(0, 32, 0, 10));
vBox.getChildren().add(passphraseEntry); vBox.getChildren().add(passphraseEntry);
@ -182,6 +193,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
AnchorPane.setRightAnchor(verifyButton, 0.0); AnchorPane.setRightAnchor(verifyButton, 0.0);
vBox.getChildren().add(buttonPane); vBox.getChildren().add(buttonPane);
}
return vBox; return vBox;
} }
@ -212,7 +224,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private void confirmBackup() { private void confirmBackup() {
setDescription("Confirm backup by re-entering words"); setDescription("Confirm backup by re-entering words");
showHideLink.setVisible(false); showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size())); setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
setExpanded(true); setExpanded(true);
} }
@ -368,4 +380,16 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
return contentBox; return contentBox;
} }
private void showWordList(DeterministicSeed seed) {
List<String> 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);
}
}
} }

View file

@ -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<Void> {
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));
}
}

View file

@ -163,6 +163,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
} }
private String decrypt(String encrypted, String password) { private String decrypt(String encrypted, String password) {
if(encrypted == null) {
return null;
}
KeyDeriver keyDeriver = new DoubleSha256KeyDeriver(); KeyDeriver keyDeriver = new DoubleSha256KeyDeriver();
Key key = keyDeriver.deriveKey(password); Key key = keyDeriver.deriveKey(password);
byte[] encryptedBytes = Base64.getDecoder().decode(encrypted); byte[] encryptedBytes = Base64.getDecoder().decode(encrypted);

View file

@ -15,6 +15,7 @@ import javafx.scene.control.DialogPane;
import org.controlsfx.tools.Borders; import org.controlsfx.tools.Borders;
import java.io.IOException; import java.io.IOException;
import java.util.List;
public class KeystoreImportDialog extends Dialog<Keystore> { public class KeystoreImportDialog extends Dialog<Keystore> {
private final KeystoreImportController keystoreImportController; private final KeystoreImportController keystoreImportController;
@ -46,6 +47,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
} }
} }
public static List<KeystoreSource> getSupportedSources() {
return List.of(KeystoreSource.HW_USB, KeystoreSource.HW_AIRGAPPED, KeystoreSource.SW_SEED);
}
@Subscribe @Subscribe
public void keystoreImported(KeystoreImportEvent event) { public void keystoreImported(KeystoreImportEvent event) {
this.keystore = event.getKeystore(); this.keystore = event.getKeystore();

View file

@ -752,7 +752,7 @@ public class HeadersController extends TransactionFormController implements Init
@Subscribe @Subscribe
public void openWallets(OpenWalletsEvent event) { public void openWallets(OpenWalletsEvent event) {
if(headersForm.getPsbt() != null) { if(headersForm.getPsbt() != null && headersForm.getBlockTransaction() == null) {
List<Wallet> availableWallets = event.getWallets().stream().filter(wallet -> wallet.canSign(headersForm.getPsbt())).collect(Collectors.toList()); List<Wallet> availableWallets = event.getWallets().stream().filter(wallet -> wallet.canSign(headersForm.getPsbt())).collect(Collectors.toList());
Map<Wallet, Storage> availableWalletsMap = new LinkedHashMap<>(event.getWalletsMap()); Map<Wallet, Storage> availableWalletsMap = new LinkedHashMap<>(event.getWalletsMap());
availableWalletsMap.keySet().retainAll(availableWallets); availableWalletsMap.keySet().retainAll(availableWallets);

View file

@ -3,10 +3,19 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource; 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.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.keystoreimport.KeystoreImportDialog;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent; import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.application.Platform; import javafx.application.Platform;
@ -14,7 +23,9 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator; import org.controlsfx.validation.Validator;
@ -37,6 +48,9 @@ public class KeystoreController extends WalletFormController implements Initiali
@FXML @FXML
private Label type; private Label type;
@FXML
private Button importButton;
@FXML @FXML
private TextField label; private TextField label;
@ -156,6 +170,15 @@ public class KeystoreController extends WalletFormController implements Initiali
private void updateType() { private void updateType() {
type.setText(getTypeLabel(keystore)); 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); boolean editable = (keystore.getSource() == KeystoreSource.SW_WATCH);
fingerprint.setEditable(editable); fingerprint.setEditable(editable);
@ -178,7 +201,12 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
public void importKeystore(ActionEvent event) { 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) { 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<SecureString> 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 @Subscribe
public void update(SettingsChangedEvent event) { public void update(SettingsChangedEvent event) {
if(walletForm.getWallet().equals(event.getWallet()) && event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) { if(walletForm.getWallet().equals(event.getWallet()) && event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) {

View file

@ -20,9 +20,9 @@
<Form fx:id="keystoreForm" GridPane.columnIndex="0" GridPane.rowIndex="0"> <Form fx:id="keystoreForm" GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text=""> <Fieldset inputGrow="SOMETIMES" text="">
<Field text="Type:"> <Field text="Type:">
<Label fx:id="type"/> <Label fx:id="type" contentDisplay="RIGHT" graphicTextGap="5" onMouseClicked="#showSeed"/>
<Pane HBox.hgrow="ALWAYS" /> <Pane HBox.hgrow="ALWAYS" />
<Button text="Import..." onAction="#importKeystore"/> <Button fx:id="importButton" text="Import..." onAction="#importKeystore"/>
</Field> </Field>
<Field text="Label:"> <Field text="Label:">
<TextField fx:id="label" maxWidth="160"/> <TextField fx:id="label" maxWidth="160"/>