recover slip39 shares to keystore seed and store as single slip39 share

This commit is contained in:
Craig Raw 2024-08-07 14:45:09 +02:00
parent 33d23e9ea5
commit 041b5aa34c
10 changed files with 499 additions and 52 deletions

2
drongo

@ -1 +1 @@
Subproject commit f066b5b608c4cff8873c776ef9c89e9194ccc332 Subproject commit ebcee47771bfb4b81dc19d0159a168d3bd8a824d

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.beans.property.SimpleListProperty; import javafx.beans.property.SimpleListProperty;
@ -15,10 +16,13 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane { public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
private final DeterministicSeed.Type type;
public MnemonicKeystoreDisplayPane(Keystore keystore) { 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"); 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); showHideLink.setVisible(false);
buttonBox.getChildren().clear(); buttonBox.getChildren().clear();
this.type = keystore.getSeed().getType();
showWordList(keystore.getSeed()); showWordList(keystore.getSeed());
} }
@ -29,7 +33,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
vBox.setSpacing(10); vBox.setSpacing(10);
wordsPane = new TilePane(); wordsPane = new TilePane();
wordsPane.setPrefRows(numWords / 3); wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
wordsPane.setHgap(10); wordsPane.setHgap(10);
wordsPane.setVgap(10); wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL); wordsPane.setOrientation(Orientation.VERTICAL);
@ -43,7 +47,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
wordEntriesProperty = new SimpleListProperty<>(wordEntryList); wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords); List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) { 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++) { for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
@ -57,4 +61,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
stackPane.getChildren().add(vBox); stackPane.getChildren().add(vBox);
return stackPane; return stackPane;
} }
@Override
protected WordlistProvider getWordlistProvider() {
return getWordListProvider(type);
}
} }

View file

@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode; import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed; 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.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
@ -153,6 +155,10 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected void showWordList(DeterministicSeed seed) { protected void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode(); List<String> words = seed.getMnemonicCode();
showWordList(words);
}
protected void showWordList(List<String> words) {
setContent(getMnemonicWordsEntry(words.size(), true, true)); setContent(getMnemonicWordsEntry(words.size(), true, true));
setExpanded(true); setExpanded(true);
@ -175,7 +181,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
vBox.setSpacing(10); vBox.setSpacing(10);
wordsPane = new TilePane(); wordsPane = new TilePane();
wordsPane.setPrefRows(numWords/3); wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
wordsPane.setHgap(10); wordsPane.setHgap(10);
wordsPane.setVgap(10); wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL); wordsPane.setOrientation(Orientation.VERTICAL);
@ -189,7 +195,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
wordEntriesProperty = new SimpleListProperty<>(wordEntryList); wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords); List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) { 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++) { for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1)); wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
@ -215,7 +221,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
buttonPane.getChildren().add(leftBox); buttonPane.getChildren().add(leftBox);
AnchorPane.setLeftAnchor(leftBox, 0.0); AnchorPane.setLeftAnchor(leftBox, 0.0);
validLabel = new Label("Valid checksum", getValidGlyph()); validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT); validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0); validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty()); validLabel.managedProperty().bind(validLabel.visibleProperty());
@ -224,7 +230,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
AnchorPane.setTopAnchor(validLabel, 5.0); AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0); AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", getInvalidGlyph()); invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT); invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0); invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty()); invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
@ -242,7 +248,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
empty = false; empty = false;
} }
if(!WordEntry.isValid(word)) { if(!getWordlistProvider().isValid(word)) {
validWords = false; validWords = false;
} }
} }
@ -278,13 +284,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
//nothing by default //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 { protected static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField; private final TextField wordField;
private WordEntry nextEntry; private WordEntry nextEntry;
private TextField nextField; private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) { public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
super(); super();
setAlignment(Pos.CENTER_RIGHT); setAlignment(Pos.CENTER_RIGHT);
@ -302,7 +315,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
for(int i = 0; i < words.length; i++) { for(int i = 0; i < words.length; i++) {
String word = words[i]; String word = words[i];
if(entry.nextField != null) { if(entry.nextField != null) {
if(i == words.length - 2 && isValid(word)) { if(i == words.length - 2 && wordlistProvider.isValid(word)) {
label.requestFocus(); label.requestFocus();
} else { } else {
entry.nextField.requestFocus(); entry.nextField.requestFocus();
@ -335,8 +348,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
}); });
wordField.setTextFormatter(formatter); wordField.setTextFormatter(formatter);
wordList = Bip39MnemonicCode.INSTANCE.getWordList(); AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList));
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
autoCompletionBinding.setDelay(50); autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> { autoCompletionBinding.setOnAutoCompleted(event -> {
if(nextField != null) { if(nextField != null) {
@ -357,7 +369,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
ValidationSupport validationSupport = new ValidationSupport(); ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(wordField, Validator.combine( 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) -> { wordField.textProperty().addListener((observable, oldValue, newValue) -> {
@ -378,28 +390,24 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
public void setNextField(TextField field) { public void setNextField(TextField field) {
this.nextField = field; this.nextField = field;
} }
public static boolean isValid(String word) {
return wordList.contains(word);
}
} }
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> { protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList; private final WordlistProvider wordlistProvider;
private final int wordNumber; private final int wordNumber;
private final ObservableList<String> wordEntryList; private final ObservableList<String> wordEntryList;
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) { public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
this.wordList = wordList; this.wordlistProvider = wordlistProvider;
this.wordNumber = wordNumber; this.wordNumber = wordNumber;
this.wordEntryList = wordEntryList; this.wordEntryList = wordEntryList;
} }
@Override @Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) { public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) { if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
try { try {
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1)); List<String> possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
if(!request.getUserText().isEmpty()) { if(!request.getUserText().isEmpty()) {
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText())); possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
} }
@ -412,7 +420,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
List<String> suggestions = new ArrayList<>(); List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) { if(!request.getUserText().isEmpty()) {
for(String word : wordList) { for(String word : wordlistProvider.getWordlist()) {
if(word.startsWith(request.getUserText())) { if(word.startsWith(request.getUserText())) {
suggestions.add(word); suggestions.add(word);
} }
@ -424,7 +432,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
private boolean allPreviousWordsValid() { private boolean allPreviousWordsValid() {
for(int i = 0; i < wordEntryList.size() - 1; i++) { for(int i = 0; i < wordEntryList.size() - 1; i++) {
if(!WordEntry.isValid(wordEntryList.get(i))) { if(!wordlistProvider.isValid(wordEntryList.get(i))) {
return false; return false;
} }
} }
@ -485,17 +493,53 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
} }
} }
public static Glyph getValidGlyph() { protected interface WordlistProvider {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE); List<String> getWordlist();
validGlyph.getStyleClass().add("success"); boolean isValid(String word);
validGlyph.setFontSize(12); boolean supportsPossibleLastWords();
return validGlyph; List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
} }
public static Glyph getInvalidGlyph() { private static class Bip39WordlistProvider implements WordlistProvider {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); @Override
invalidGlyph.getStyleClass().add("failure"); public List<String> getWordlist() {
invalidGlyph.setFontSize(12); return Bip39MnemonicCode.INSTANCE.getWordList();
return invalidGlyph; }
public boolean isValid(String word) {
return getWordlist().contains(word);
}
@Override
public boolean supportsPossibleLastWords() {
return true;
}
@Override
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords);
}
}
private static class Slip39WordlistProvider implements WordlistProvider {
@Override
public List<String> getWordlist() {
return Slip39MnemonicCode.INSTANCE.getWordList();
}
@Override
public boolean isValid(String word) {
return getWordlist().contains(word);
}
@Override
public boolean supportsPossibleLastWords() {
return false;
}
@Override
public List<String> getPossibleLastWords(List<String> previousWords) {
throw new UnsupportedOperationException();
}
} }
} }

View file

@ -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<List<String>> 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<Node> 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<List<String>> 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<ChildNumber> 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<ChildNumber> 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<ChildNumber> 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<String> 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<ChildNumber> 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<ChildNumber> 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);
}
}

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.SeedQR; import com.sparrowwallet.drongo.wallet.SeedQR;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
@ -17,7 +18,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow()); 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; int height = lines * 40;
StackPane stackPane = new StackPane(); StackPane stackPane = new StackPane();
@ -43,15 +44,19 @@ public class SeedDisplayDialog extends Dialog<Void> {
stackPane.getChildren().addAll(anchorPane); stackPane.getChildren().addAll(anchorPane);
final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT); if(decryptedKeystore.getSeed().getType() == DeterministicSeed.Type.BIP39) {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().addAll(seedQRButtonType, cancelButtonType); dialogPane.getButtonTypes().add(seedQRButtonType);
Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType); Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType);
seedQRButton.addEventFilter(ActionEvent.ACTION, event -> { seedQRButton.addEventFilter(ActionEvent.ACTION, event -> {
event.consume(); event.consume();
showSeedQR(decryptedKeystore); showSeedQR(decryptedKeystore);
}); });
}
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().add(cancelButtonType);
dialogPane.setPrefWidth(500); dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(150 + height); dialogPane.setPrefHeight(150 + height);

View file

@ -183,6 +183,13 @@ public class GlyphUtils {
return successGlyph; 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() { public static Glyph getWarningGlyph() {
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE); Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warningGlyph.getStyleClass().add("warn-icon"); warningGlyph.getStyleClass().add("warn-icon");

View file

@ -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<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
}

View file

@ -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<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
try {
RecoveryState recoveryState = new RecoveryState();
for(List<String> 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;
}
}
}

View file

@ -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()); 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); seed.setId(id);
} else { } 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); seed.setId(id);
} }
} }
@ -96,7 +96,7 @@ public interface KeystoreDao {
EncryptedData data = seed.getEncryptedData(); 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()); 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 { } 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());
} }
} }
} }

View file

@ -1,9 +1,6 @@
package com.sparrowwallet.sparrow.keystoreimport; package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.control.MnemonicKeystoreImportPane;
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
import com.sparrowwallet.sparrow.control.XprvKeystoreImportPane;
import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.io.*;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Accordion; import javafx.scene.control.Accordion;
@ -15,7 +12,7 @@ public class SwController extends KeystoreImportDetailController {
private Accordion importAccordion; private Accordion importAccordion;
public void initializeView() { public void initializeView() {
List<KeystoreImport> importers = List.of(new Bip39(), new Electrum(), new Bip32()); List<KeystoreImport> importers = List.of(new Bip39(), new Bip32(), new Slip39());
for(KeystoreImport importer : importers) { for(KeystoreImport importer : importers) {
if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) { if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) {
@ -30,6 +27,8 @@ public class SwController extends KeystoreImportDetailController {
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer, getMasterController().getDefaultDerivation()); importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer, getMasterController().getDefaultDerivation());
} else if(importer instanceof KeystoreXprvImport) { } else if(importer instanceof KeystoreXprvImport) {
importPane = new XprvKeystoreImportPane(getMasterController().getWallet(), (KeystoreXprvImport)importer, getMasterController().getDefaultDerivation()); importPane = new XprvKeystoreImportPane(getMasterController().getWallet(), (KeystoreXprvImport)importer, getMasterController().getDefaultDerivation());
} else if(importer instanceof KeystoreMnemonicShareImport) {
importPane = new MnemonicShareKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicShareImport)importer, getMasterController().getDefaultDerivation());
} else { } else {
throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass()); throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass());
} }