import seed via border wallets grid pattern

This commit is contained in:
Craig Raw 2023-03-27 11:00:32 +02:00
parent fd2b383dbc
commit af532e7fc9
8 changed files with 280 additions and 2 deletions

View file

@ -160,6 +160,7 @@ run {
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx", "--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow", "--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson", "--add-opens=java.base/java.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow"] "--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow"]
@ -208,6 +209,7 @@ jlink {
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx", "--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx", "--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow", "--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson", "--add-opens=java.base/java.io=com.google.gson",

View file

@ -0,0 +1,196 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.PdfUtils;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.controlsfx.control.spreadsheet.*;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class MnemonicGridDialog extends Dialog<List<String>> {
private final SpreadsheetView spreadsheetView;
private final BooleanProperty initializedProperty = new SimpleBooleanProperty(false);
private final BooleanProperty wordsSelectedProperty = new SimpleBooleanProperty(false);
public MnemonicGridDialog() {
DialogPane dialogPane = new MnemonicGridDialogPane();
setDialogPane(dialogPane);
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!");
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
dialogPane.setGraphic(new ImageView(image));
String[][] emptyWordGrid = new String[128][16];
Grid grid = getGrid(emptyWordGrid);
spreadsheetView = new SpreadsheetView(grid);
spreadsheetView.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
try {
Field f = event.getClass().getDeclaredField(Platform.getCurrent() == Platform.OSX ? "metaDown" : "controlDown");
f.setAccessible(true);
f.set(event, true);
} catch(IllegalAccessException | NoSuchFieldException e) {
//ignore
}
});
spreadsheetView.setId("grid");
spreadsheetView.setEditable(false);
spreadsheetView.setFixingColumnsAllowed(false);
spreadsheetView.setFixingRowsAllowed(false);
spreadsheetView.getSelectionModel().getSelectedCells().addListener(new ListChangeListener<>() {
@Override
public void onChanged(Change<? extends TablePosition> c) {
int numWords = c.getList().size();
wordsSelectedProperty.set(numWords == 11 || numWords == 23);
}
});
StackPane stackPane = new StackPane();
stackPane.getChildren().add(spreadsheetView);
dialogPane.setContent(stackPane);
stackPane.widthProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
for(SpreadsheetColumn column : spreadsheetView.getColumns()) {
column.setPrefWidth((newValue.doubleValue() - spreadsheetView.getRowHeaderWidth() - 3) / 17);
}
}
});
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(loadCsvButtonType);
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
okButton.disableProperty().bind(Bindings.not(Bindings.and(initializedProperty, wordsSelectedProperty)));
setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? getSelectedWords() : null;
});
dialogPane.setPrefWidth(850);
dialogPane.setPrefHeight(500);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.moveToActiveWindowScreen(this);
}
private Grid getGrid(String[][] wordGrid) {
int rowCount = wordGrid.length;
int columnCount = wordGrid[0].length;
GridBase grid = new GridBase(rowCount, columnCount);
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
grid.getColumnHeaders().setAll("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P");
for(int i = 0; i < rowCount; i++) {
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
for(int j = 0; j < columnCount; j++) {
list.add(createCell(i, j, wordGrid[i][j]));
}
rows.add(list);
grid.getRowHeaders().add(String.format("%03d", i + 1));
}
grid.setRows(rows);
return grid;
}
private SpreadsheetCell createCell(int row, int column, String word) {
return SpreadsheetCellType.STRING.createCell(row, column, 1, 1, word == null ? "" : word);
}
private List<String> getSelectedWords() {
List<String> abbreviations = spreadsheetView.getSelectionModel().getSelectedCells().stream()
.map(position -> (String)spreadsheetView.getGrid().getRows().get(position.getRow()).get(position.getColumn()).getItem()).collect(Collectors.toList());
List<String> words = new ArrayList<>();
for(String abbreviation : abbreviations) {
for(String word : Bip39MnemonicCode.INSTANCE.getWordList()) {
if((abbreviation.length() == 3 && word.equals(abbreviation)) || (abbreviation.length() >= 4 && word.startsWith(abbreviation))) {
words.add(word);
break;
}
}
}
if(words.size() != abbreviations.size()) {
abbreviations.removeIf(abbr -> words.stream().anyMatch(w -> w.startsWith(abbr)));
throw new IllegalStateException("Could not find words for abbreviations: " + abbreviations);
}
return words;
}
private class MnemonicGridDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
Node button;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
Button loadButton = new Button(buttonType.getText());
loadButton.setGraphicTextGap(5);
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(loadButton, buttonData);
loadButton.setOnAction(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open PDF");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("PDF", "*.pdf")
);
AppServices.moveToActiveWindowScreen(this.getScene().getWindow(), 800, 450);
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
if(file != null) {
try(BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
String[][] wordGrid = PdfUtils.getWordGrid(inputStream);
spreadsheetView.setGrid(getGrid(wordGrid));
initializedProperty.set(true);
} catch(Exception e) {
AppServices.showErrorDialog("Cannot load PDF", e.getMessage());
}
}
});
button = loadButton;
} else {
button = super.createButton(buttonType);
}
return button;
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
}

View file

@ -12,6 +12,8 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -20,6 +22,7 @@ import javafx.scene.control.*;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Callback; import javafx.util.Callback;
import javafx.util.Duration;
import org.controlsfx.control.textfield.AutoCompletionBinding; import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.TextFields; import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
@ -78,6 +81,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
enterMnemonicButton.getItems().add(item); enterMnemonicButton.getItems().add(item);
} }
enterMnemonicButton.getItems().add(new SeparatorMenuItem()); enterMnemonicButton.getItems().add(new SeparatorMenuItem());
MenuItem gridItem = new MenuItem("Border Wallets...");
gridItem.setOnAction(event -> {
showGrid();
});
enterMnemonicButton.getItems().add(gridItem);
MenuItem scanItem = new MenuItem("Scan QR..."); MenuItem scanItem = new MenuItem("Scan QR...");
scanItem.setOnAction(event -> { scanItem.setOnAction(event -> {
scanQR(); scanQR();
@ -86,6 +95,42 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty()); enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
} }
protected void showGrid() {
MnemonicGridDialog mnemonicGridDialog = new MnemonicGridDialog();
Optional<List<String>> optWords = mnemonicGridDialog.showAndWait();
if(optWords.isPresent()) {
List<String> words = optWords.get();
setContent(getMnemonicWordsEntry(words.size() + 1, true));
setExpanded(true);
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
if(i < words.size()) {
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
} else {
ScheduledService<Void> service = new ScheduledService<>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() {
return null;
}
};
}
};
service.setDelay(Duration.millis(500));
service.setOnSucceeded(event1 -> {
service.cancel();
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
});
service.start();
}
}
}
}
protected void scanQR() { protected void scanQR() {
QRScanDialog qrScanDialog = new QRScanDialog(); QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait(); Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();

View file

@ -13,6 +13,7 @@ import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfWriter; import com.lowagie.text.pdf.PdfWriter;
import com.lowagie.text.pdf.parser.PdfTextExtractor; import com.lowagie.text.pdf.parser.PdfTextExtractor;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder; import com.sparrowwallet.hummingbird.UREncoder;
@ -25,8 +26,8 @@ import org.slf4j.LoggerFactory;
import java.awt.*; import java.awt.*;
import java.io.*; import java.io.*;
import java.util.Locale; import java.util.*;
import java.util.Scanner; import java.util.List;
public class PdfUtils { public class PdfUtils {
private static final Logger log = LoggerFactory.getLogger(PdfUtils.class); private static final Logger log = LoggerFactory.getLogger(PdfUtils.class);
@ -108,4 +109,33 @@ public class PdfUtils {
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
return new javafx.scene.image.Image(bais); return new javafx.scene.image.Image(bais);
} }
public static String[][] getWordGrid(InputStream inputStream) {
try {
PdfReader pdfReader = new PdfReader(inputStream);
String allText = "";
for(int page = 1; page <= pdfReader.getNumberOfPages(); page++) {
PdfTextExtractor textExtractor = new PdfTextExtractor(pdfReader);
allText += textExtractor.getTextFromPage(page) + "\n";
}
List<String[]> rows = new ArrayList<>();
Scanner scanner = new Scanner(allText);
while(scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
String[] words = line.split(" ");
if(words.length > 16 && Utils.isNumber(words[0])) {
rows.add(Arrays.copyOfRange(words, 1, 17));
}
}
if(rows.size() < 128) {
throw new IllegalArgumentException("Not a valid Border Wallets PDF");
}
return rows.toArray(new String[][]{new String[0]});
} catch(Exception e) {
throw new IllegalArgumentException("Not a valid Border Wallets PDF");
}
}
} }

View file

@ -0,0 +1,5 @@
#grid .spreadsheet-cell:selected,
#grid .spreadsheet-cell:focused:selected,
#grid .spreadsheet-cell:focused:selected:hover {
-fx-background-color: rgb(238, 210, 2);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB