From 041b5aa34c4aaf5ee789352312ca8348d1516480 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 7 Aug 2024 14:45:09 +0200 Subject: [PATCH] recover slip39 shares to keystore seed and store as single slip39 share --- drongo | 2 +- .../control/MnemonicKeystoreDisplayPane.java | 13 +- .../sparrow/control/MnemonicKeystorePane.java | 110 ++++-- .../MnemonicShareKeystoreImportPane.java | 313 ++++++++++++++++++ .../sparrow/control/SeedDisplayDialog.java | 23 +- .../sparrow/glyphfont/GlyphUtils.java | 7 + .../io/KeystoreMnemonicShareImport.java | 10 + .../com/sparrowwallet/sparrow/io/Slip39.java | 60 ++++ .../sparrow/io/db/KeystoreDao.java | 4 +- .../sparrow/keystoreimport/SwController.java | 9 +- 10 files changed, 499 insertions(+), 52 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/MnemonicShareKeystoreImportPane.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/KeystoreMnemonicShareImport.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/Slip39.java diff --git a/drongo b/drongo index f066b5b6..ebcee477 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit f066b5b608c4cff8873c776ef9c89e9194ccc332 +Subproject commit ebcee47771bfb4b81dc19d0159a168d3bd8a824d diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java index d2b90fea..a48e7c18 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.control; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.WalletModel; import javafx.beans.property.SimpleListProperty; @@ -15,10 +16,13 @@ import java.util.ArrayList; import java.util.List; public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane { + private final DeterministicSeed.Type type; + public MnemonicKeystoreDisplayPane(Keystore keystore) { super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png"); showHideLink.setVisible(false); buttonBox.getChildren().clear(); + this.type = keystore.getSeed().getType(); showWordList(keystore.getSeed()); } @@ -29,7 +33,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane { vBox.setSpacing(10); wordsPane = new TilePane(); - wordsPane.setPrefRows(numWords / 3); + wordsPane.setPrefRows(Math.ceilDiv(numWords, 3)); wordsPane.setHgap(10); wordsPane.setVgap(10); wordsPane.setOrientation(Orientation.VERTICAL); @@ -43,7 +47,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane { wordEntriesProperty = new SimpleListProperty<>(wordEntryList); List wordEntries = new ArrayList<>(numWords); for(int i = 0; i < numWords; i++) { - wordEntries.add(new WordEntry(i, wordEntryList)); + wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider())); } for(int i = 0; i < numWords - 1; i++) { wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); @@ -57,4 +61,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane { stackPane.getChildren().add(vBox); return stackPane; } + + @Override + protected WordlistProvider getWordlistProvider() { + return getWordListProvider(type); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java index 016a8f88..9643c36e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java @@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode; import com.sparrowwallet.drongo.wallet.DeterministicSeed; +import com.sparrowwallet.drongo.wallet.MnemonicException; +import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.glyphfont.GlyphUtils; import javafx.application.Platform; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; @@ -153,6 +155,10 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { protected void showWordList(DeterministicSeed seed) { List words = seed.getMnemonicCode(); + showWordList(words); + } + + protected void showWordList(List words) { setContent(getMnemonicWordsEntry(words.size(), true, true)); setExpanded(true); @@ -175,7 +181,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { vBox.setSpacing(10); wordsPane = new TilePane(); - wordsPane.setPrefRows(numWords/3); + wordsPane.setPrefRows(Math.ceilDiv(numWords, 3)); wordsPane.setHgap(10); wordsPane.setVgap(10); wordsPane.setOrientation(Orientation.VERTICAL); @@ -189,7 +195,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { wordEntriesProperty = new SimpleListProperty<>(wordEntryList); List wordEntries = new ArrayList<>(numWords); for(int i = 0; i < numWords; i++) { - wordEntries.add(new WordEntry(i, wordEntryList)); + wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider())); } for(int i = 0; i < numWords - 1; i++) { wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); @@ -215,7 +221,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { buttonPane.getChildren().add(leftBox); AnchorPane.setLeftAnchor(leftBox, 0.0); - validLabel = new Label("Valid checksum", getValidGlyph()); + validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph()); validLabel.setContentDisplay(ContentDisplay.LEFT); validLabel.setGraphicTextGap(5.0); validLabel.managedProperty().bind(validLabel.visibleProperty()); @@ -224,7 +230,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { AnchorPane.setTopAnchor(validLabel, 5.0); AnchorPane.setLeftAnchor(validLabel, 0.0); - invalidLabel = new Label("Invalid checksum", getInvalidGlyph()); + invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph()); invalidLabel.setContentDisplay(ContentDisplay.LEFT); invalidLabel.setGraphicTextGap(5.0); invalidLabel.managedProperty().bind(invalidLabel.visibleProperty()); @@ -242,7 +248,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { empty = false; } - if(!WordEntry.isValid(word)) { + if(!getWordlistProvider().isValid(word)) { validWords = false; } } @@ -278,13 +284,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { //nothing by default } + protected WordlistProvider getWordlistProvider() { + return getWordListProvider(DeterministicSeed.Type.BIP39); + } + + protected WordlistProvider getWordListProvider(DeterministicSeed.Type type) { + return type == DeterministicSeed.Type.SLIP39 ? new Slip39WordlistProvider() : new Bip39WordlistProvider(); + } + protected static class WordEntry extends HBox { - private static List wordList; private final TextField wordField; private WordEntry nextEntry; private TextField nextField; - public WordEntry(int wordNumber, ObservableList wordEntryList) { + public WordEntry(int wordNumber, ObservableList wordEntryList, WordlistProvider wordlistProvider) { super(); setAlignment(Pos.CENTER_RIGHT); @@ -302,7 +315,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { for(int i = 0; i < words.length; i++) { String word = words[i]; if(entry.nextField != null) { - if(i == words.length - 2 && isValid(word)) { + if(i == words.length - 2 && wordlistProvider.isValid(word)) { label.requestFocus(); } else { entry.nextField.requestFocus(); @@ -335,8 +348,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { }); wordField.setTextFormatter(formatter); - wordList = Bip39MnemonicCode.INSTANCE.getWordList(); - AutoCompletionBinding autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList)); + AutoCompletionBinding autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList)); autoCompletionBinding.setDelay(50); autoCompletionBinding.setOnAutoCompleted(event -> { if(nextField != null) { @@ -357,7 +369,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { ValidationSupport validationSupport = new ValidationSupport(); validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); validationSupport.registerValidator(wordField, Validator.combine( - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue)) + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordlistProvider.isValid(newValue)) )); wordField.textProperty().addListener((observable, oldValue, newValue) -> { @@ -378,28 +390,24 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { public void setNextField(TextField field) { this.nextField = field; } - - public static boolean isValid(String word) { - return wordList.contains(word); - } } protected static class WordlistSuggestionProvider implements Callback> { - private final List wordList; + private final WordlistProvider wordlistProvider; private final int wordNumber; private final ObservableList wordEntryList; - public WordlistSuggestionProvider(List wordList, int wordNumber, ObservableList wordEntryList) { - this.wordList = wordList; + public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList wordEntryList) { + this.wordlistProvider = wordlistProvider; this.wordNumber = wordNumber; this.wordEntryList = wordEntryList; } @Override public Collection call(AutoCompletionBinding.ISuggestionRequest request) { - if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) { + if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) { try { - List possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1)); + List possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1)); if(!request.getUserText().isEmpty()) { possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText())); } @@ -412,7 +420,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { List suggestions = new ArrayList<>(); if(!request.getUserText().isEmpty()) { - for(String word : wordList) { + for(String word : wordlistProvider.getWordlist()) { if(word.startsWith(request.getUserText())) { suggestions.add(word); } @@ -424,7 +432,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { private boolean allPreviousWordsValid() { for(int i = 0; i < wordEntryList.size() - 1; i++) { - if(!WordEntry.isValid(wordEntryList.get(i))) { + if(!wordlistProvider.isValid(wordEntryList.get(i))) { return false; } } @@ -485,17 +493,53 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { } } - public static Glyph getValidGlyph() { - Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE); - validGlyph.getStyleClass().add("success"); - validGlyph.setFontSize(12); - return validGlyph; + protected interface WordlistProvider { + List getWordlist(); + boolean isValid(String word); + boolean supportsPossibleLastWords(); + List getPossibleLastWords(List previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException; } - public static Glyph getInvalidGlyph() { - Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); - invalidGlyph.getStyleClass().add("failure"); - invalidGlyph.setFontSize(12); - return invalidGlyph; + private static class Bip39WordlistProvider implements WordlistProvider { + @Override + public List getWordlist() { + return Bip39MnemonicCode.INSTANCE.getWordList(); + } + + public boolean isValid(String word) { + return getWordlist().contains(word); + } + + @Override + public boolean supportsPossibleLastWords() { + return true; + } + + @Override + public List getPossibleLastWords(List previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException { + return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords); + } + } + + private static class Slip39WordlistProvider implements WordlistProvider { + @Override + public List getWordlist() { + return Slip39MnemonicCode.INSTANCE.getWordList(); + } + + @Override + public boolean isValid(String word) { + return getWordlist().contains(word); + } + + @Override + public boolean supportsPossibleLastWords() { + return false; + } + + @Override + public List getPossibleLastWords(List previousWords) { + throw new UnsupportedOperationException(); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicShareKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicShareKeystoreImportPane.java new file mode 100644 index 00000000..c8df30ba --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicShareKeystoreImportPane.java @@ -0,0 +1,313 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.MnemonicException; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.slip39.Share; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.KeystoreImportEvent; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.glyphfont.GlyphUtils; +import com.sparrowwallet.sparrow.io.ImportException; +import com.sparrowwallet.sparrow.io.KeystoreMnemonicShareImport; +import com.sparrowwallet.sparrow.io.Slip39; +import javafx.beans.property.SimpleIntegerProperty; +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.glyphfont.Glyph; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.controlsfx.validation.decoration.StyleClassValidationDecoration; + +import java.util.*; + +public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane { + protected final Wallet wallet; + private final KeystoreMnemonicShareImport importer; + private final KeyDerivation defaultDerivation; + private final List> mnemonicShares = new ArrayList<>(); + + private SplitMenuButton importButton; + + private Button calculateButton; + private Button backButton; + private Button nextButton; + private int currentShare; + + public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) { + super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); + this.wallet = wallet; + this.importer = importer; + this.defaultDerivation = defaultDerivation; + + createImportButton(); + buttonBox.getChildren().add(importButton); + } + + @Override + protected Control createButton() { + createEnterMnemonicButton(); + return enterMnemonicButton; + } + + private void createEnterMnemonicButton() { + enterMnemonicButton = new SplitMenuButton(); + enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT); + enterMnemonicButton.setText("Use 20 Words"); + defaultWordSizeProperty = new SimpleIntegerProperty(20); + defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> { + enterMnemonicButton.setText("Use " + newValue + " Words"); + }); + enterMnemonicButton.setOnAction(event -> { + resetShares(); + enterMnemonic(defaultWordSizeProperty.get()); + }); + int[] numberWords = new int[] {20, 33}; + for(int i = 0; i < numberWords.length; i++) { + MenuItem item = new MenuItem("Use " + numberWords[i] + " Words"); + final int words = numberWords[i]; + item.setOnAction(event -> { + resetShares(); + defaultWordSizeProperty.set(words); + enterMnemonic(words); + }); + enterMnemonicButton.getItems().add(item); + } + enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty()); + } + + protected List createRightButtons() { + calculateButton = new Button("Create Keystore"); + calculateButton.setDefaultButton(true); + calculateButton.setOnAction(event -> { + prepareImport(); + }); + calculateButton.managedProperty().bind(calculateButton.visibleProperty()); + calculateButton.setTooltip(new Tooltip("Create the keystore from the provided shares")); + calculateButton.setVisible(false); + + backButton = new Button("Back"); + backButton.setOnAction(event -> { + lastShare(); + }); + backButton.managedProperty().bind(backButton.visibleProperty()); + backButton.setTooltip(new Tooltip("Display the last share added")); + backButton.setVisible(currentShare > 0); + + nextButton = new Button("Next"); + nextButton.setOnAction(event -> { + nextShare(); + }); + nextButton.managedProperty().bind(nextButton.visibleProperty()); + nextButton.setTooltip(new Tooltip("Add the next share")); + nextButton.visibleProperty().bind(calculateButton.visibleProperty().not()); + nextButton.setDefaultButton(true); + nextButton.setDisable(true); + + return List.of(backButton, nextButton, calculateButton); + } + + private void resetShares() { + currentShare = 0; + mnemonicShares.clear(); + } + + private void lastShare() { + currentShare--; + showWordList(mnemonicShares.get(currentShare)); + } + + private void nextShare() { + if(currentShare == mnemonicShares.size()) { + mnemonicShares.add(wordEntriesProperty.get()); + } else { + mnemonicShares.set(currentShare, wordEntriesProperty.get()); + } + + currentShare++; + + if(currentShare < mnemonicShares.size()) { + showWordList(mnemonicShares.get(currentShare)); + } else { + setContent(getMnemonicWordsEntry(defaultWordSizeProperty.get(), true, true)); + } + setExpanded(true); + } + + protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) { + boolean validSet = false; + boolean complete = false; + if(!empty && validWords) { + try { + Share.fromMnemonic(String.join(" ", wordEntriesProperty.get())); + validChecksum = true; + + List> existing = new ArrayList<>(mnemonicShares); + if(currentShare >= mnemonicShares.size()) { + existing.add(wordEntriesProperty.get()); + } + + importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get()); + validSet = true; + complete = true; + } catch(MnemonicException e) { + invalidLabel.setText(e.getTitle()); + invalidLabel.setTooltip(new Tooltip(e.getMessage())); + } catch(Slip39.Slip39ProgressException e) { + validSet = true; + invalidLabel.setText(e.getTitle()); + invalidLabel.setTooltip(new Tooltip(e.getMessage())); + } catch(ImportException e) { + if(e.getCause() instanceof MnemonicException mnemonicException) { + invalidLabel.setText(mnemonicException.getTitle()); + invalidLabel.setTooltip(new Tooltip(mnemonicException.getMessage())); + } else { + invalidLabel.setText("Import Error"); + invalidLabel.setTooltip(new Tooltip(e.getMessage())); + } + } + } + + calculateButton.setVisible(complete); + backButton.setVisible(currentShare > 0 && !complete); + nextButton.setDisable(!validChecksum || !validSet); + validLabel.setVisible(complete); + validLabel.setText(mnemonicShares.isEmpty() ? "Valid checksum" : "Completed share set"); + invalidLabel.setVisible(!complete && !empty); + invalidLabel.setGraphic(validChecksum && validSet ? getIncompleteGlyph() : GlyphUtils.getFailureGlyph()); + } + + 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(getDefaultDerivation(), false); + }); + 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, false); + }); + importButton.getItems().add(item); + } + + importButton.managedProperty().bind(importButton.visibleProperty()); + importButton.setVisible(false); + } + + private List getDefaultDerivation() { + return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation(); + } + + private void prepareImport() { + nextShare(); + backButton.setVisible(false); + + if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) { + setExpanded(true); + enterMnemonicButton.setVisible(false); + importButton.setVisible(true); + importButton.setDisable(false); + setDescription("Ready to import"); + showHideLink.setText("Show Derivation..."); + showHideLink.setVisible(false); + setContent(getDerivationEntry(getDefaultDerivation())); + } + } + + private boolean importKeystore(List derivation, boolean dryrun) { + importButton.setDisable(true); + try { + Keystore keystore = importer.getKeystore(derivation, mnemonicShares, passphraseProperty.get()); + if(!dryrun) { + if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) { + KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true); + keystorePassphraseDialog.initOwner(this.getScene().getWindow()); + Optional optPassphrase = keystorePassphraseDialog.showAndWait(); + if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) { + throw new ImportException("Re-entered passphrase did not match"); + } + } + + EventManager.get().post(new KeystoreImportEvent(keystore)); + } + return true; + } catch (ImportException e) { + String errorMessage = e.getMessage(); + if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) { + errorMessage = "Invalid word list - checksum incorrect"; + } else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { + errorMessage = e.getCause().getMessage(); + } + setError("Import Error", errorMessage + "."); + importButton.setDisable(false); + return false; + } + } + + 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.setValidationDecorator(new StyleClassValidationDecoration()); + validationSupport.registerValidator(derivationField, Validator.combine( + Validator.createEmptyValidator("Derivation is required"), + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue)) + )); + + 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, false); + }); + + 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; + } + + public static Glyph getIncompleteGlyph() { + Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE); + warningGlyph.getStyleClass().add("warn-icon"); + warningGlyph.setFontSize(12); + return warningGlyph; + } + + @Override + protected WordlistProvider getWordlistProvider() { + return getWordListProvider(DeterministicSeed.Type.SLIP39); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java index 06e7788d..4095c1db 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/SeedDisplayDialog.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.control; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.SeedQR; import com.sparrowwallet.sparrow.AppServices; @@ -17,7 +18,7 @@ public class SeedDisplayDialog extends Dialog { dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); AppServices.setStageIcon(dialogPane.getScene().getWindow()); - int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3; + int lines = Math.ceilDiv(decryptedKeystore.getSeed().getMnemonicCode().size(), 3); int height = lines * 40; StackPane stackPane = new StackPane(); @@ -43,15 +44,19 @@ public class SeedDisplayDialog extends Dialog { stackPane.getChildren().addAll(anchorPane); - final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT); - final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); - dialogPane.getButtonTypes().addAll(seedQRButtonType, cancelButtonType); + if(decryptedKeystore.getSeed().getType() == DeterministicSeed.Type.BIP39) { + final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT); + dialogPane.getButtonTypes().add(seedQRButtonType); - Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType); - seedQRButton.addEventFilter(ActionEvent.ACTION, event -> { - event.consume(); - showSeedQR(decryptedKeystore); - }); + Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType); + seedQRButton.addEventFilter(ActionEvent.ACTION, event -> { + event.consume(); + showSeedQR(decryptedKeystore); + }); + } + + final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + dialogPane.getButtonTypes().add(cancelButtonType); dialogPane.setPrefWidth(500); dialogPane.setPrefHeight(150 + height); diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java index cdf421fd..656ced10 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java @@ -183,6 +183,13 @@ public class GlyphUtils { return successGlyph; } + public static Glyph getInvalidGlyph() { + Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); + invalidGlyph.getStyleClass().add("failure"); + invalidGlyph.setFontSize(12); + return invalidGlyph; + } + public static Glyph getWarningGlyph() { Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE); warningGlyph.getStyleClass().add("warn-icon"); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoreMnemonicShareImport.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreMnemonicShareImport.java new file mode 100644 index 00000000..3b3866eb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreMnemonicShareImport.java @@ -0,0 +1,10 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.wallet.Keystore; + +import java.util.List; + +public interface KeystoreMnemonicShareImport extends KeystoreImport { + Keystore getKeystore(List derivation, List> mnemonicShares, String passphrase) throws ImportException; +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Slip39.java b/src/main/java/com/sparrowwallet/sparrow/io/Slip39.java new file mode 100644 index 00000000..e0afd31d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/Slip39.java @@ -0,0 +1,60 @@ +package com.sparrowwallet.sparrow.io; + +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.drongo.wallet.slip39.RecoveryState; +import com.sparrowwallet.drongo.wallet.slip39.Share; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class Slip39 implements KeystoreMnemonicShareImport { + @Override + public String getName() { + return "Mnemonic Shares (SLIP39)"; + } + + @Override + public WalletModel getWalletModel() { + return WalletModel.SEED; + } + + @Override + public String getKeystoreImportDescription() { + return "Import your 20 or 33 word mnemonic shares and optional passphrase."; + } + + @Override + public Keystore getKeystore(List derivation, List> mnemonicShares, String passphrase) throws ImportException { + try { + RecoveryState recoveryState = new RecoveryState(); + for(List mnemonicWords : mnemonicShares) { + Share share = Share.fromMnemonic(String.join(" ", mnemonicWords)); + recoveryState.addShare(share); + } + + if(recoveryState.isComplete()) { + byte[] secret = recoveryState.recover(passphrase.getBytes(StandardCharsets.UTF_8)); + DeterministicSeed seed = new DeterministicSeed(secret, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.SLIP39); + return Keystore.fromSeed(seed, derivation); + } else { + throw new Slip39ProgressException(recoveryState.getShortStatus(), recoveryState.getStatus()); + } + } catch(MnemonicException e) { + throw new ImportException(e); + } + } + + public static class Slip39ProgressException extends ImportException { + private final String title; + + public Slip39ProgressException(String title, String message) { + super(message); + this.title = title; + } + + public String getTitle() { + return title; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java index 0823b167..27366863 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/KeystoreDao.java @@ -63,7 +63,7 @@ public interface KeystoreDao { long id = insertSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds()); seed.setId(id); } else { - long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds()); + long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString(true).asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds()); seed.setId(id); } } @@ -96,7 +96,7 @@ public interface KeystoreDao { EncryptedData data = seed.getEncryptedData(); updateSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId()); } else { - updateSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId()); + updateSeed(seed.getType().ordinal(), seed.getMnemonicString(true).asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId()); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java index 035630c9..68b669e8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/SwController.java @@ -1,9 +1,6 @@ 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.control.*; import com.sparrowwallet.sparrow.io.*; import javafx.fxml.FXML; import javafx.scene.control.Accordion; @@ -15,7 +12,7 @@ public class SwController extends KeystoreImportDetailController { private Accordion importAccordion; public void initializeView() { - List importers = List.of(new Bip39(), new Electrum(), new Bip32()); + List importers = List.of(new Bip39(), new Bip32(), new Slip39()); for(KeystoreImport importer : importers) { if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) { @@ -30,6 +27,8 @@ public class SwController extends KeystoreImportDetailController { importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer, getMasterController().getDefaultDerivation()); } else if(importer instanceof KeystoreXprvImport) { importPane = new XprvKeystoreImportPane(getMasterController().getWallet(), (KeystoreXprvImport)importer, getMasterController().getDefaultDerivation()); + } else if(importer instanceof KeystoreMnemonicShareImport) { + importPane = new MnemonicShareKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicShareImport)importer, getMasterController().getDefaultDerivation()); } else { throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass()); }