usb keystore importing

This commit is contained in:
Craig Raw 2020-04-29 12:03:44 +02:00
parent 431a170ab0
commit 9adfcf5806
33 changed files with 1196 additions and 19 deletions

View file

@ -16,7 +16,7 @@ repositories {
javafx {
version = "14"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing' ]
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
java {
@ -32,6 +32,7 @@ dependencies {
implementation('com.google.code.gson:gson:2.8.6')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('org.apache.commons:commons-compress:1.20')
implementation('org.controlsfx:controlsfx:11.0.1' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'

2
drongo

@ -1 +1 @@
Subproject commit 294649de669497283934933487d09e1dae9f3996
Subproject commit ed056bc49fb919799f70a6d7ee2dd65a38c25b5d

View file

@ -1,6 +1,11 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
@ -14,6 +19,7 @@ public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
Parent root = transactionLoader.load();
@ -30,7 +36,14 @@ public class MainApp extends Application {
appController.initializeView();
stage.show();
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2PKH);
KeystoreImportDialog dlg = new KeystoreImportDialog(wallet);
dlg.showAndWait();
//stage.show();
}
public static void main(String[] args) {

View file

@ -0,0 +1,32 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.external.Device;
import javafx.collections.ObservableList;
import javafx.scene.control.Accordion;
public class DeviceAccordion extends Accordion {
private ObservableList<Device> devices;
private DeviceOperation deviceOperation = DeviceOperation.IMPORT;
public void setDevices(Wallet wallet, ObservableList<Device> devices) {
this.devices = devices;
for(Device device : devices) {
DevicePane devicePane = new DevicePane(this, wallet, device);
this.getPanes().add(devicePane);
}
}
public DeviceOperation getDeviceOperation() {
return deviceOperation;
}
public void setDeviceOperation(DeviceOperation deviceOperation) {
this.deviceOperation = deviceOperation;
}
public enum DeviceOperation {
IMPORT;
}
}

View file

@ -0,0 +1,388 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.ExtendedPublicKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.external.Device;
import com.sparrowwallet.sparrow.external.Hwi;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import java.util.List;
public class DevicePane extends TitledPane {
private final DeviceAccordion deviceAccordion;
private final Wallet wallet;
private final Device device;
private Label mainLabel;
private Label statusLabel;
private CustomPasswordField pinField;
private CustomTextField passphraseField;
private Button unlockButton;
private Button enterPinButton;
private Button setPassphraseButton;
private SplitMenuButton importButton;
private final SimpleStringProperty status = new SimpleStringProperty("");
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
public DevicePane(DeviceAccordion deviceAccordion, Wallet wallet, Device device) {
super();
this.deviceAccordion = deviceAccordion;
this.wallet = wallet;
this.device = device;
setPadding(new Insets(0, 0, 0, 0));
setGraphic(getTitle());
getStyleClass().add("devicepane");
setDefaultStatus();
Platform.runLater(() -> {
Node arrow = this.lookup(".arrow");
if(arrow != null) {
arrow.setVisible(false);
arrow.setManaged(false);
}
});
}
private void setDefaultStatus() {
setStatus(device.getNeedsPinSent() ? "Locked" : device.getNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
}
private Node getTitle() {
HBox listItem = new HBox();
listItem.setPadding(new Insets(10, 20, 10, 10));
listItem.setSpacing(10);
HBox imageBox = new HBox();
imageBox.setMinWidth(50);
imageBox.setMinHeight(50);
listItem.getChildren().add(imageBox);
Image image = new Image("image/" + device.getType() + ".png", 50, 50, true, true);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setImage(image);
imageBox.getChildren().add(imageView);
}
VBox labelsBox = new VBox();
labelsBox.setSpacing(5);
labelsBox.setAlignment(Pos.CENTER_LEFT);
this.mainLabel = new Label();
mainLabel.setText(device.getModel().toDisplayString());
mainLabel.getStyleClass().add("devicelist-main-label");
labelsBox.getChildren().add(mainLabel);
this.statusLabel = new Label();
statusLabel.textProperty().bind(status);
labelsBox.getChildren().add(statusLabel);
statusLabel.getStyleClass().add("devicelist-status-label");
listItem.getChildren().add(labelsBox);
HBox.setHgrow(labelsBox, Priority.ALWAYS);
HBox buttonBox = new HBox();
buttonBox.setAlignment(Pos.CENTER_RIGHT);
createUnlockButton();
createSetPassphraseButton();
createImportButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
buttonBox.getChildren().addAll(unlockButton, setPassphraseButton, importButton);
listItem.getChildren().add(buttonBox);
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
listItem.setPrefWidth(newValue.getWidth());
});
return listItem;
}
private void createUnlockButton() {
unlockButton = new Button("Unlock");
unlockButton.setAlignment(Pos.CENTER_RIGHT);
unlockButton.setOnAction(event -> {
unlockButton.setDisable(true);
unlock(device);
});
unlockButton.managedProperty().bind(unlockButton.visibleProperty());
unlockButton.setVisible(false);
}
private void createSetPassphraseButton() {
setPassphraseButton = new Button("Set Passphrase");
setPassphraseButton.setAlignment(Pos.CENTER_RIGHT);
setPassphraseButton.setOnAction(event -> {
setPassphraseButton.setDisable(true);
setContent(getPassphraseEntry());
setExpanded(true);
});
setPassphraseButton.managedProperty().bind(setPassphraseButton.visibleProperty());
setPassphraseButton.setVisible(false);
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType().getDefaultDerivation());
});
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
for(int i = 0; i < accounts.length; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private void unlock(Device device) {
if(device.getModel().equals(WalletModel.TREZOR_1)) {
promptPin();
}
}
private Node getPinEntry() {
VBox vBox = new VBox();
vBox.setMaxHeight(120);
vBox.setSpacing(42);
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
enterPinButton = new Button("Enter PIN");
enterPinButton.setOnAction(event -> {
enterPinButton.setDisable(true);
sendPin(pinField.getText());
});
vBox.getChildren().addAll(pinField, enterPinButton);
TilePane tilePane = new TilePane();
tilePane.setPrefColumns(3);
tilePane.setHgap(10);
tilePane.setVgap(10);
tilePane.setMaxWidth(150);
tilePane.setMaxHeight(120);
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
for(int i = 0; i < digits.length; i++) {
Button pinButton = new Button();
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
pinButton.setGraphic(circle);
pinButton.setUserData(digits[i]);
tilePane.getChildren().add(pinButton);
pinButton.setOnAction(event -> {
pinField.setText(pinField.getText() + pinButton.getUserData());
});
}
HBox contentBox = new HBox();
contentBox.setSpacing(50);
contentBox.getChildren().add(tilePane);
contentBox.getChildren().add(vBox);
contentBox.setPadding(new Insets(10, 0, 10, 0));
contentBox.setAlignment(Pos.TOP_CENTER);
return contentBox;
}
private Node getPassphraseEntry() {
passphraseField = (CustomTextField)TextFields.createClearableTextField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
Button sendPassphraseButton = new Button("Send Passphrase");
sendPassphraseButton.setOnAction(event -> {
setExpanded(false);
sendPassphrase(passphrase.get());
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(passphraseField);
contentBox.getChildren().add(sendPassphraseButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
return contentBox;
}
private void promptPin() {
Hwi.PromptPinService promptPinService = new Hwi.PromptPinService(device);
promptPinService.setOnSucceeded(workerStateEvent -> {
Boolean result = promptPinService.getValue();
if(result) {
setContent(getPinEntry());
setExpanded(true);
} else {
setErrorStatus("Could not request PIN");
unlockButton.setDisable(false);
}
});
promptPinService.setOnFailed(workerStateEvent -> {
setErrorStatus(promptPinService.getException().getMessage());
unlockButton.setDisable(false);
});
promptPinService.start();
}
private void sendPin(String pin) {
Hwi.SendPinService sendPinService = new Hwi.SendPinService(device, pin);
sendPinService.setOnSucceeded(workerStateEvent -> {
Boolean result = sendPinService.getValue();
if(result) {
device.setNeedsPinSent(false);
setDefaultStatus();
setExpanded(false);
unlockButton.setVisible(false);
if(device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
setPassphraseButton.setDisable(true);
setContent(getPassphraseEntry());
setExpanded(true);
} else {
showOperationButton();
}
} else {
setErrorStatus("Incorrect PIN");
enterPinButton.setDisable(false);
if(pinField != null) {
pinField.setText("");
}
}
});
sendPinService.setOnFailed(workerStateEvent -> {
setErrorStatus(sendPinService.getException().getMessage());
enterPinButton.setDisable(false);
});
sendPinService.start();
}
private void sendPassphrase(String passphrase) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase);
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
for (Device freshDevice : devices) {
if (device.getPath().equals(freshDevice.getPath()) && device.getModel().equals(freshDevice.getModel())) {
device.setFingerprint(freshDevice.getFingerprint());
}
}
if(device.getFingerprint() != null) {
setPassphraseButton.setVisible(false);
device.setNeedsPassphraseSent(false);
setDefaultStatus();
showOperationButton();
} else {
setErrorStatus("Passphrase send failed");
setPassphraseButton.setDisable(false);
setPassphraseButton.setVisible(true);
}
});
enumerateService.setOnFailed(workerStateEvent -> {
setErrorStatus(enumerateService.getException().getMessage());
setPassphraseButton.setDisable(false);
setPassphraseButton.setVisible(true);
});
enumerateService.start();
}
private void importKeystore(List<ChildNumber> derivation) {
if(device.getFingerprint() == null) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
for (Device freshDevice : devices) {
if (device.getPath().equals(freshDevice.getPath()) && device.getModel().equals(freshDevice.getModel())) {
device.setFingerprint(freshDevice.getFingerprint());
}
}
importXpub(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setErrorStatus(enumerateService.getException().getMessage());
importButton.setDisable(false);
});
enumerateService.start();
} else {
importXpub(derivation);
}
}
private void importXpub(List<ChildNumber> derivation) {
String derivationPath = KeyDerivation.writePath(derivation);
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
getXpubService.setOnSucceeded(workerStateEvent -> {
String xpub = getXpubService.getValue();
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString() + " " + device.getFingerprint().toUpperCase());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(xpub));
EventManager.get().post(new KeystoreImportEvent(keystore));
});
getXpubService.setOnFailed(workerStateEvent -> {
setErrorStatus(getXpubService.getException().getMessage());
importButton.setDisable(false);
});
getXpubService.start();
}
private void setStatus(String statusMessage) {
statusLabel.getStyleClass().remove("status-error");
status.setValue(statusMessage);
}
private void setErrorStatus(String statusMessage) {
statusLabel.getStyleClass().add("status-error");
status.setValue(statusMessage);
}
private void showOperationButton() {
if(deviceAccordion.getDeviceOperation().equals(DeviceAccordion.DeviceOperation.IMPORT)) {
importButton.setVisible(true);
} else {
//TODO: Support further device operations such as signing
}
}
}

View file

@ -1,14 +0,0 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.NamedArg;
import javafx.scene.control.ChoiceBox;
public class EnumChoiceBox<E extends Enum<E>> extends ChoiceBox<E> {
public EnumChoiceBox(@NamedArg("enumType") String enumType) throws Exception {
Class<E> enumClass = (Class<E>) Class.forName(enumType);
getItems().setAll(enumClass.getEnumConstants());
}
}

View file

@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
public class KeystoreImportEvent {
private Keystore keystore;
public KeystoreImportEvent(Keystore keystore) {
this.keystore = keystore;
}
public Keystore getKeystore() {
return keystore;
}
}

View file

@ -56,7 +56,7 @@ public class ColdcardSinglesig implements SinglesigWalletImport {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
if(!key.equals("m") && scriptType.getDefaultDerivation().startsWith(key)) {
if(!key.equals("m") && scriptType.getDefaultDerivationPath().startsWith(key)) {
ExtendedPublicKey extPubKey = ExtendedPublicKey.fromDescriptor(value);
Keystore keystore = new Keystore();
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, key));

View file

@ -0,0 +1,64 @@
package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.wallet.WalletModel;
public class Device {
private String type;
private String path;
private WalletModel model;
private Boolean needsPinSent;
private Boolean needsPassphraseSent;
private String fingerprint;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public WalletModel getModel() {
return model;
}
public void setModel(WalletModel model) {
this.model = model;
}
public Boolean getNeedsPinSent() {
return needsPinSent;
}
public void setNeedsPinSent(Boolean needsPinSent) {
this.needsPinSent = needsPinSent;
}
public Boolean getNeedsPassphraseSent() {
return needsPassphraseSent;
}
public void setNeedsPassphraseSent(Boolean needsPassphraseSent) {
this.needsPassphraseSent = needsPassphraseSent;
}
public String getFingerprint() {
return fingerprint;
}
public void setFingerprint(String fingerprint) {
this.fingerprint = fingerprint;
}
public String toString() {
return getModel() + ":" + getPath();
}
}

View file

@ -0,0 +1,251 @@
package com.sparrowwallet.sparrow.external;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.gson.*;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream;
import org.controlsfx.tools.Platform;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
public class Hwi implements KeystoreImport {
private static File hwiExecutable;
@Override
public String getName() {
return "Hardware Wallet";
}
@Override
public String getKeystoreImportDescription() {
return "Imports a connected hardware wallet";
}
@Override
public PolicyType getPolicyType() {
return PolicyType.SINGLE;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException {
return null;
}
public List<Device> enumerate(String passphrase) throws ImportException {
try {
List<String> command;
if(passphrase != null) {
command = List.of(getHwiExecutable().getAbsolutePath(), "--password", passphrase, "enumerate");
} else {
command = List.of(getHwiExecutable().getAbsolutePath(), "enumerate");
}
String output = execute(command);
Device[] devices = getGson().fromJson(output, Device[].class);
return Arrays.asList(devices);
} catch(IOException e) {
throw new ImportException(e);
}
}
public boolean promptPin(Device device) throws ImportException {
try {
String output = execute(getDeviceCommand(device, "promptpin"));
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
}
}
public boolean sendPin(Device device, String pin) throws ImportException {
try {
String output = execute(getDeviceCommand(device, "sendpin", pin));
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
}
}
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
String output;
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
output = execute(getDeviceCommand(device, passphrase, "getxpub", derivationPath));
} else {
output = execute(getDeviceCommand(device, "getxpub", derivationPath));
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("xpub") != null) {
return result.get("xpub").getAsString();
} else {
throw new ImportException("Could not retrieve xpub");
}
} catch(IOException e) {
throw new ImportException(e);
}
}
private String execute(List<String> command) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();
return CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
}
private synchronized File getHwiExecutable() throws IOException {
if(hwiExecutable == null) {
Platform platform = Platform.getCurrent();
System.out.println("/external/" + platform.getPlatformId().toLowerCase() + "/hwi");
InputStream inputStream = Hwi.class.getResourceAsStream("/external/" + platform.getPlatformId().toLowerCase() + "/hwi");
Set<PosixFilePermission> ownerExecutableWritable = PosixFilePermissions.fromString("rwxr--r--");
Path tempExecPath = Files.createTempFile("hwi", null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
File tempExec = tempExecPath.toFile();
System.out.println(tempExec.getAbsolutePath());
tempExec.deleteOnExit();
OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec));
ByteStreams.copy(new FramedLZ4CompressorInputStream(inputStream), tempExecStream);
inputStream.close();
tempExecStream.flush();
tempExecStream.close();
hwiExecutable = tempExec;
}
return hwiExecutable;
}
private boolean wasSuccessful(String output) throws ImportException {
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("error") != null) {
throw new ImportException(result.get("error").getAsString());
}
return result.get("success").getAsBoolean();
}
private List<String> getDeviceCommand(Device device, String command) throws IOException {
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command);
}
private List<String> getDeviceCommand(Device device, String command, String data) throws IOException {
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command, data);
}
private List<String> getDeviceCommand(Device device, String passphrase, String command, String data) throws IOException {
return List.of(getHwiExecutable().getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command, data);
}
public static class EnumerateService extends Service<List<Device>> {
private final String passphrase;
public EnumerateService(String passphrase) {
this.passphrase = passphrase;
}
@Override
protected Task<List<Device>> createTask() {
return new Task<>() {
protected List<Device> call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.enumerate(passphrase);
}
};
}
}
public static class PromptPinService extends Service<Boolean> {
private Device device;
public PromptPinService(Device device) {
this.device = device;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.promptPin(device);
}
};
}
}
public static class SendPinService extends Service<Boolean> {
private Device device;
private String pin;
public SendPinService(Device device, String pin) {
this.device = device;
this.pin = pin;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.sendPin(device, pin);
}
};
}
}
public static class GetXpubService extends Service<String> {
private Device device;
private String passphrase;
private String derivationPath;
public GetXpubService(Device device, String passphrase, String derivationPath) {
this.device = device;
this.passphrase = passphrase;
this.derivationPath = derivationPath;
}
@Override
protected Task<String> createTask() {
return new Task<>() {
protected String call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.getXpub(device, passphrase, derivationPath);
}
};
}
}
public Gson getGson() {
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelDeserializer());
return gsonBuilder.create();
}
private static class DeviceModelSerializer implements JsonSerializer<WalletModel> {
@Override
public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase());
}
}
private static class DeviceModelDeserializer implements JsonDeserializer<WalletModel> {
@Override
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return WalletModel.valueOf(json.getAsJsonPrimitive().getAsString().toUpperCase());
}
}
}

View file

@ -15,6 +15,10 @@ public class FontAwesome5 extends GlyphFont {
* The individual glyphs offered by the FontAwesome5 font.
*/
public static enum Glyph implements INamedCharacter {
CIRCLE('\uf111'),
EXCLAMATION_CIRCLE('\uf06a'),
LAPTOP('\uf109'),
SD_CARD('\uf7c2'),
WALLET('\uf555');
private final char ch;

View file

@ -0,0 +1,57 @@
package com.sparrowwallet.sparrow.glyphfont;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.GlyphFont;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.controlsfx.glyphfont.INamedCharacter;
import java.io.InputStream;
import java.util.Arrays;
public class FontAwesome5Brands extends GlyphFont {
public static String FONT_NAME = "Font Awesome 5 Brands Regular";
/**
* The individual glyphs offered by the FontAwesome5Brands font.
*/
public static enum Glyph implements INamedCharacter {
USB('\uf287');
private final char ch;
/**
* Creates a named Glyph mapped to the given character
*
* @param ch
*/
Glyph(char ch) {
this.ch = ch;
}
@Override
public char getChar() {
return ch;
}
}
/**
* Do not call this constructor directly - instead access the
* {@link FontAwesome5Brands.Glyph} public static enumeration method to create the glyph nodes), or
* use the {@link GlyphFontRegistry} class to get access.
* <p>
* Note: Do not remove this public constructor since it is used by the service loader!
*/
public FontAwesome5Brands() {
this(FontAwesome5Brands.class.getResourceAsStream("/font/fa-brands-400.ttf"));
}
/**
* Creates a new FontAwesome5Brands instance which uses the provided font source.
*
* @param is
*/
public FontAwesome5Brands(InputStream is) {
super(FONT_NAME, 14, is, true);
registerAll(Arrays.asList(FontAwesome5Brands.Glyph.values()));
}
}

View file

@ -0,0 +1,72 @@
package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.external.Device;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.StackPane;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;
public class KeystoreImportController implements Initializable {
private Wallet wallet;
@FXML
private ToggleGroup importMenu;
@FXML
private StackPane importPane;
@Override
public void initialize(URL location, ResourceBundle resources) {
}
public Wallet getWallet() {
return wallet;
}
public void initializeView(Wallet wallet) {
this.wallet = wallet;
importMenu.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> {
KeystoreSource importType = (KeystoreSource) selectedToggle.getUserData();
setImportPane(importType.toString().toLowerCase());
});
}
void showUsbDevices(List<Device> devices) {
FXMLLoader loader = setImportPane("usb-devices");
UsbDevicesController controller = loader.getController();
controller.initializeView(devices);
}
void showUsbError(String message) {
FXMLLoader loader = setImportPane("usb-error");
UsbScanController controller = loader.getController();
controller.initializeView(message);
}
FXMLLoader setImportPane(String fxmlName) {
importPane.getChildren().removeAll(importPane.getChildren());
try {
FXMLLoader importLoader = new FXMLLoader(AppController.class.getResource("keystoreimport/" + fxmlName + ".fxml"));
Node importTypeNode = importLoader.load();
KeystoreImportDetailController controller = importLoader.getController();
controller.setMasterController(this);
importPane.getChildren().add(importTypeNode);
return importLoader;
} catch (IOException e) {
throw new IllegalStateException("Can't find pane", e);
}
}
}

View file

@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.keystoreimport;
public abstract class KeystoreImportDetailController {
private KeystoreImportController masterController;
public KeystoreImportController getMasterController() {
return masterController;
}
void setMasterController(KeystoreImportController masterController) {
this.masterController = masterController;
}
}

View file

@ -0,0 +1,48 @@
package com.sparrowwallet.sparrow.keystoreimport;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import org.controlsfx.tools.Borders;
import java.io.IOException;
public class KeystoreImportDialog extends Dialog<Keystore> {
private final KeystoreImportController keystoreImportController;
private Keystore keystore;
public KeystoreImportDialog(Wallet wallet) {
EventManager.get().register(this);
final DialogPane dialogPane = getDialogPane();
try {
FXMLLoader ksiLoader = new FXMLLoader(AppController.class.getResource("keystoreimport/keystoreimport.fxml"));
dialogPane.setContent(Borders.wrap(ksiLoader.load()).lineBorder().outerPadding(0).innerPadding(0).buildAll());
keystoreImportController = ksiLoader.getController();
keystoreImportController.initializeView(wallet);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(620);
dialogPane.setPrefHeight(500);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
@Subscribe
public void keystoreImported(KeystoreImportEvent event) {
this.keystore = event.getKeystore();
this.close();
}
}

View file

@ -0,0 +1,18 @@
package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.sparrow.control.DeviceAccordion;
import com.sparrowwallet.sparrow.external.Device;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import java.util.List;
public class UsbDevicesController extends KeystoreImportDetailController {
@FXML
private DeviceAccordion deviceAccordion;
public void initializeView(List<Device> devices) {
deviceAccordion.setDeviceOperation(DeviceAccordion.DeviceOperation.IMPORT);
deviceAccordion.setDevices(getMasterController().getWallet(), FXCollections.observableList(devices));
}
}

View file

@ -0,0 +1,38 @@
package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.sparrow.external.Device;
import com.sparrowwallet.sparrow.external.Hwi;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import java.util.List;
public class UsbScanController extends KeystoreImportDetailController {
@FXML
private Label message;
@FXML
private Button scan;
public void initializeView(String updateMessage) {
message.setText(updateMessage);
}
public void scan(ActionEvent event) {
message.setText("Please check your device");
scan.setText("Scanning...");
scan.setDisable(true);
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
getMasterController().showUsbDevices(devices);
});
enumerateService.setOnFailed(workerStateEvent -> {
getMasterController().showUsbError(enumerateService.getException().getMessage());
});
enumerateService.start();
}
}

View file

@ -5,8 +5,10 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Control;
@ -18,6 +20,7 @@ import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.net.URL;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
@ -36,7 +39,7 @@ public class KeystoreController extends WalletFormController implements Initiali
@FXML
private TextField fingerprint;
private ValidationSupport validationSupport = new ValidationSupport();
private final ValidationSupport validationSupport = new ValidationSupport();
@Override
public void initialize(URL location, ResourceBundle resources) {
@ -119,4 +122,16 @@ public class KeystoreController extends WalletFormController implements Initiali
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
}
public void importKeystore(ActionEvent event) {
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet());
Optional<Keystore> result = dlg.showAndWait();
if(result.isPresent()) {
Keystore keystore = result.get();
label.setText(keystore.getLabel());
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
xpub.setText(keystore.getExtendedPublicKey().toString());
}
}
}

View file

@ -2,6 +2,7 @@ open module com.sparrowwallet.sparrow {
requires java.desktop;
requires javafx.controls;
requires javafx.fxml;
requires javafx.graphics;
requires org.controlsfx.controls;
requires org.fxmisc.richtext;
requires tornadofx.controls;
@ -9,5 +10,6 @@ open module com.sparrowwallet.sparrow {
requires com.google.common;
requires flowless;
requires com.google.gson;
requires org.apache.commons.compress;
requires javafx.swing;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,60 @@
.dialog-pane .content {
-fx-padding: 0;
}
.list-menu {
-fx-pref-width: 130;
-fx-background-color: #3da0e3;
}
.list-item {
-fx-pref-width: 130;
-fx-padding: 0 20 0 20;
-fx-background-color: #3da0e3;
}
.list-item * {
-fx-fill: #fff;
}
.list-item:hover {
-fx-background-color: #4aa7e5;
}
.list-item:selected {
-fx-background-color: #1e88cf;
}
#importPane {
-fx-background-color: -fx-background;
}
.scroll-pane {
-fx-background-color: transparent;
}
.titled-pane > .title {
-fx-background-color: white;
-fx-padding: 0;
}
.titled-pane > .title > .arrow-button {
-fx-padding: 0;
}
.titled-pane > .title > .arrow-button > .arrow {
visibility: hidden;
-fx-translate-x: -1000;
}
.devicelist-main-label .text {
}
.devicelist-status-label .text {
-fx-fill: #a0a1a7;
}
.status-error .text {
-fx-fill: #ca1243;
}

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.controlsfx.glyphfont.Glyph?>
<?import com.sparrowwallet.drongo.wallet.KeystoreSource?>
<?import javafx.geometry.Insets?>
<BorderPane stylesheets="@../general.css, @keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.KeystoreImportController">
<padding>
<Insets top="0" left="0" right="0" bottom="0" />
</padding>
<left>
<VBox styleClass="list-menu">
<ToggleButton VBox.vgrow="ALWAYS" text="Connected Hardware Wallet" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity">
<toggleGroup>
<ToggleGroup fx:id="importMenu" />
</toggleGroup>
<graphic>
<Glyph fontFamily="Font Awesome 5 Brands Regular" fontSize="20" icon="USB" />
</graphic>
<userData>
<KeystoreSource fx:constant="HW_USB"/>
</userData>
</ToggleButton>
<ToggleButton VBox.vgrow="ALWAYS" text="Airgapped Wallet" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$importMenu">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="SD_CARD" />
</graphic>
<userData>
<KeystoreSource fx:constant="HW_AIRGAPPED"/>
</userData>
</ToggleButton>
<ToggleButton VBox.vgrow="ALWAYS" text="Software Wallet" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$importMenu">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="LAPTOP" />
</graphic>
<userData>
<KeystoreSource fx:constant="SW_SEED"/>
</userData>
</ToggleButton>
</VBox>
</left>
<center>
<StackPane fx:id="importPane">
</StackPane>
</center>
</BorderPane>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.sparrowwallet.sparrow.control.DeviceAccordion?>
<AnchorPane stylesheets="@keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.UsbDevicesController">
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true">
<DeviceAccordion fx:id="deviceAccordion" />
</ScrollPane>
</AnchorPane>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.controlsfx.glyphfont.Glyph?>
<VBox alignment="CENTER" spacing="30" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.UsbScanController">
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="50" icon="EXCLAMATION_CIRCLE" />
<Label text="There was error connecting to the wallet:" />
<Label fx:id="message" />
<Button fx:id="scan" text="Scan Again..." wrapText="true" prefWidth="120" prefHeight="60" onAction="#scan"/>
</VBox>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.image.ImageView?>
<?import org.controlsfx.glyphfont.Glyph?>
<VBox alignment="CENTER" spacing="30" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.UsbScanController">
<Glyph fontFamily="Font Awesome 5 Brands Regular" fontSize="50" icon="USB" />
<Label fx:id="message" text="Connect Hardware Wallet" />
<Button fx:id="scan" text="Scan..." wrapText="true" prefWidth="120" prefHeight="60" onAction="#scan"/>
</VBox>

View file

@ -18,6 +18,8 @@
<Fieldset inputGrow="SOMETIMES" text="">
<Field text="Label:">
<TextField fx:id="label" maxWidth="160"/>
<Pane HBox.hgrow="ALWAYS" />
<Button text="Import..." onAction="#importKeystore"/>
</Field>
<Field text="Master fingerprint:">
<TextField fx:id="fingerprint" maxWidth="80"/>

BIN
src/main/resources/external/mac/hwi vendored Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB