From 6063b0211372766c907dd85dc831f42106b37423 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 29 Mar 2023 08:21:15 +0200 Subject: [PATCH] generate border wallets grid from seed words --- .../sparrow/control/MnemonicGridDialog.java | 63 +++++++++++- .../control/MnemonicKeystoreDisplayPane.java | 3 +- .../control/MnemonicKeystoreEntryPane.java | 95 +++++++++++++++++++ .../control/MnemonicKeystoreImportPane.java | 2 +- .../sparrow/control/MnemonicKeystorePane.java | 20 ++-- .../sparrow/control/SeedEntryDialog.java | 91 ++++++++++++++++++ 6 files changed, 259 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreEntryPane.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/SeedEntryDialog.java diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicGridDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicGridDialog.java index 8293db03..ccbdccb8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicGridDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicGridDialog.java @@ -27,6 +27,7 @@ import java.io.FileInputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Random; import java.util.stream.Collectors; @@ -34,6 +35,8 @@ import java.util.stream.Collectors; public class MnemonicGridDialog extends Dialog> { private final SpreadsheetView spreadsheetView; + private final int GRID_COLUMN_COUNT = 16; + private final BooleanProperty initializedProperty = new SimpleBooleanProperty(false); private final BooleanProperty wordsSelectedProperty = new SimpleBooleanProperty(false); @@ -43,11 +46,11 @@ public class MnemonicGridDialog extends Dialog> { setTitle("Border Wallets Grid"); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm()); - dialogPane.setHeaderText("Load a Border Wallets PDF, and select 11 or 23 words in the grid.\nThe order of selection is important!"); + dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid. Note the order of selection is important!"); javafx.scene.image.Image image = new Image("/image/border-wallets.png"); dialogPane.setGraphic(new ImageView(image)); - String[][] emptyWordGrid = new String[128][16]; + String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT]; Grid grid = getGrid(emptyWordGrid); spreadsheetView = new SpreadsheetView(grid); @@ -87,9 +90,15 @@ public class MnemonicGridDialog extends Dialog> { dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF", ButtonBar.ButtonData.LEFT); + final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF...", ButtonBar.ButtonData.LEFT); dialogPane.getButtonTypes().add(loadCsvButtonType); + final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate Grid...", ButtonBar.ButtonData.HELP); + dialogPane.getButtonTypes().add(generateButtonType); + + final ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear Selection", ButtonBar.ButtonData.OTHER); + dialogPane.getButtonTypes().add(clearButtonType); + Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK); okButton.disableProperty().bind(Bindings.not(Bindings.and(initializedProperty, wordsSelectedProperty))); @@ -166,6 +175,24 @@ public class MnemonicGridDialog extends Dialog> { return words; } + private String[][] toGrid(List words) { + String[][] grid = new String[words.size()/GRID_COLUMN_COUNT][GRID_COLUMN_COUNT]; + + int row = 0; + int col = 0; + for(String word : words) { + String abbr = word.length() < 4 ? word : word.substring(0, 4); + grid[row][col] = abbr; + col++; + if(col >= GRID_COLUMN_COUNT) { + col = 0; + row++; + } + } + + return grid; + } + private class MnemonicGridDialogPane extends DialogPane { @Override protected Node createButton(ButtonType buttonType) { @@ -198,6 +225,36 @@ public class MnemonicGridDialog extends Dialog> { }); button = loadButton; + } else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP) { + Button generateButton = new Button(buttonType.getText()); + final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); + ButtonBar.setButtonData(generateButton, buttonData); + generateButton.setOnAction(event -> { + SeedEntryDialog seedEntryDialog = new SeedEntryDialog(12); + Optional> optWords = seedEntryDialog.showAndWait(); + if(optWords.isPresent()) { + List mnemonicWords = optWords.get(); + List shuffledWordList = shuffle(mnemonicWords); + String[][] wordGrid = toGrid(shuffledWordList); + spreadsheetView.setGrid(getGrid(wordGrid)); + initializedProperty.set(true); + + if(seedEntryDialog.isGenerated()) { + //TODO: Save grid PDF + } + } + }); + + button = generateButton; + } else if(buttonType.getButtonData() == ButtonBar.ButtonData.OTHER) { + Button clearButton = new Button(buttonType.getText()); + final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); + ButtonBar.setButtonData(clearButton, buttonData); + clearButton.setOnAction(event -> { + spreadsheetView.getSelectionModel().clearSelection(); + }); + + button = clearButton; } else { button = super.createButton(buttonType); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java index 87748f67..d2b90fea 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreDisplayPane.java @@ -1,6 +1,5 @@ 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; @@ -25,7 +24,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane { } @Override - protected Node getMnemonicWordsEntry(int numWords, boolean editPassphrase) { + protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) { VBox vBox = new VBox(); vBox.setSpacing(10); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreEntryPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreEntryPane.java new file mode 100644 index 00000000..b61f37f1 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreEntryPane.java @@ -0,0 +1,95 @@ +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.WalletModel; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.scene.Node; +import javafx.scene.control.Button; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.List; + +public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane { + private final BooleanProperty validProperty = new SimpleBooleanProperty(false); + + private boolean generated; + + public MnemonicKeystoreEntryPane(int numWords) { + super(DeterministicSeed.Type.BIP39.getName(), "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png"); + showHideLink.setVisible(false); + buttonBox.getChildren().clear(); + + defaultWordSizeProperty.set(numWords); + setDescription("Generate new or enter existing"); + showHideLink.setVisible(false); + setContent(getMnemonicWordsEntry(numWords, false, true)); + setExpanded(true); + } + + @Override + protected List createRightButtons() { + Button button = new Button("Next"); + button.setVisible(false); + return List.of(button); + } + + @Override + protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) { + if(!empty && validWords) { + try { + Bip39MnemonicCode.INSTANCE.check(wordEntriesProperty.get()); + validChecksum = true; + } catch(MnemonicException e) { + invalidLabel.setText("Invalid checksum"); + invalidLabel.setTooltip(null); + } + } + + validProperty.set(validChecksum); + validLabel.setVisible(validChecksum); + invalidLabel.setVisible(!validChecksum && !empty); + } + + public void generateNew() { + int mnemonicSeedLength = wordEntriesProperty.get().size() * 11; + int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33); + + SecureRandom secureRandom; + try { + secureRandom = SecureRandom.getInstanceStrong(); + } catch(NoSuchAlgorithmException e) { + secureRandom = new SecureRandom(); + } + + DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, ""); + displayMnemonicCode(deterministicSeed); + generated = true; + } + + private void displayMnemonicCode(DeterministicSeed deterministicSeed) { + setDescription("Write down these words"); + showHideLink.setVisible(false); + + for (int i = 0; i < wordsPane.getChildren().size(); i++) { + WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i); + wordEntry.getEditor().setText(deterministicSeed.getMnemonicCode().get(i)); + wordEntry.getEditor().setEditable(false); + } + } + + public boolean isValid() { + return validProperty.get(); + } + + public BooleanProperty validProperty() { + return validProperty; + } + + public boolean isGenerated() { + return generated; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java index ac388e55..6e1c1418 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java @@ -223,7 +223,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane { private void confirmBackup() { setDescription("Confirm backup by re-entering words"); showHideLink.setVisible(false); - setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false)); + setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), true, false)); setExpanded(true); backButton.setVisible(true); generateButton.setVisible(false); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java index 7ef944c8..b2816931 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystorePane.java @@ -81,7 +81,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { enterMnemonicButton.getItems().add(item); } enterMnemonicButton.getItems().add(new SeparatorMenuItem()); - MenuItem gridItem = new MenuItem("Border Wallets..."); + MenuItem gridItem = new MenuItem("Border Wallets Grid..."); gridItem.setOnAction(event -> { showGrid(); }); @@ -100,7 +100,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { Optional> optWords = mnemonicGridDialog.showAndWait(); if(optWords.isPresent()) { List words = optWords.get(); - setContent(getMnemonicWordsEntry(words.size() + 1, true)); + setContent(getMnemonicWordsEntry(words.size() + 1, true, true)); setExpanded(true); for(int i = 0; i < wordsPane.getChildren().size(); i++) { @@ -150,7 +150,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { protected void showWordList(DeterministicSeed seed) { List words = seed.getMnemonicCode(); - setContent(getMnemonicWordsEntry(words.size(), true)); + setContent(getMnemonicWordsEntry(words.size(), true, true)); setExpanded(true); for(int i = 0; i < wordsPane.getChildren().size(); i++) { @@ -163,11 +163,11 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { protected void enterMnemonic(int numWords) { setDescription("Generate new or enter existing"); showHideLink.setVisible(false); - setContent(getMnemonicWordsEntry(numWords, true)); + setContent(getMnemonicWordsEntry(numWords, true, true)); setExpanded(true); } - protected Node getMnemonicWordsEntry(int numWords, boolean editPassphrase) { + protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) { VBox vBox = new VBox(); vBox.setSpacing(10); @@ -196,10 +196,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane { vBox.getChildren().add(wordsPane); - PassphraseEntry passphraseEntry = new PassphraseEntry(editPassphrase); - wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor()); - passphraseEntry.setPadding(new Insets(0, 26, 10, 10)); - vBox.getChildren().add(passphraseEntry); + if(showPassphrase) { + PassphraseEntry passphraseEntry = new PassphraseEntry(editPassphrase); + wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor()); + passphraseEntry.setPadding(new Insets(0, 26, 10, 10)); + vBox.getChildren().add(passphraseEntry); + } AnchorPane buttonPane = new AnchorPane(); buttonPane.setPadding(new Insets(0, 26, 0, 10)); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SeedEntryDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SeedEntryDialog.java new file mode 100644 index 00000000..1cd3b9ca --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/SeedEntryDialog.java @@ -0,0 +1,91 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.AppServices; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; + +import java.util.List; + +public class SeedEntryDialog extends Dialog> { + private final MnemonicKeystoreEntryPane keystorePane; + + public SeedEntryDialog(int numWords) { + final DialogPane dialogPane = new MnemonicGridDialogPane(); + setDialogPane(dialogPane); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + + int lines = numWords / 3; + int height = lines * 40; + + StackPane stackPane = new StackPane(); + dialogPane.setContent(stackPane); + + AnchorPane anchorPane = new AnchorPane(); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.getStyleClass().add("edge-to-edge"); + scrollPane.setPrefHeight(104 + 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); + + keystorePane = new MnemonicKeystoreEntryPane(numWords); + keystorePane.setAnimated(false); + keystoreAccordion.getPanes().add(keystorePane); + + stackPane.getChildren().addAll(anchorPane); + + dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK); + okButton.disableProperty().bind(keystorePane.validProperty().not()); + + final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate New", ButtonBar.ButtonData.LEFT); + dialogPane.getButtonTypes().add(generateButtonType); + + setResultConverter((dialogButton) -> { + ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData(); + return data == ButtonBar.ButtonData.OK_DONE ? keystorePane.wordEntriesProperty.get() : null; + }); + + dialogPane.setPrefWidth(500); + dialogPane.setPrefHeight(180 + height); + AppServices.moveToActiveWindowScreen(this); + + Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane)); + } + + private class MnemonicGridDialogPane extends DialogPane { + @Override + protected Node createButton(ButtonType buttonType) { + Node button; + if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) { + Button generateButton = new Button(buttonType.getText()); + final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); + ButtonBar.setButtonData(generateButton, buttonData); + generateButton.setOnAction(event -> { + keystorePane.generateNew(); + }); + + button = generateButton; + } else { + button = super.createButton(buttonType); + } + + return button; + } + } + + public boolean isGenerated() { + return keystorePane.isGenerated(); + } +}