import airgapped keystores

This commit is contained in:
Craig Raw 2020-04-29 15:04:44 +02:00
parent 40d31ff025
commit 728b6efbdf
19 changed files with 340 additions and 45 deletions

2
drongo

@ -1 +1 @@
Subproject commit ed056bc49fb919799f70a6d7ee2dd65a38c25b5d Subproject commit d740895bd68f0ded97e382e5944feb26fa759875

View file

@ -37,8 +37,8 @@ public class MainApp extends Application {
appController.initializeView(); appController.initializeView();
Wallet wallet = new Wallet(); Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE); wallet.setPolicyType(PolicyType.MULTI);
wallet.setScriptType(ScriptType.P2PKH); wallet.setScriptType(ScriptType.P2SH);
KeystoreImportDialog dlg = new KeystoreImportDialog(wallet); KeystoreImportDialog dlg = new KeystoreImportDialog(wallet);
dlg.showAndWait(); dlg.showAndWait();

View file

@ -51,7 +51,7 @@ public class DevicePane extends TitledPane {
this.wallet = wallet; this.wallet = wallet;
this.device = device; this.device = device;
setPadding(new Insets(0, 0, 0, 0)); setPadding(Insets.EMPTY);
setGraphic(getTitle()); setGraphic(getTitle());
getStyleClass().add("devicepane"); getStyleClass().add("devicepane");
@ -92,14 +92,14 @@ public class DevicePane extends TitledPane {
labelsBox.setAlignment(Pos.CENTER_LEFT); labelsBox.setAlignment(Pos.CENTER_LEFT);
this.mainLabel = new Label(); this.mainLabel = new Label();
mainLabel.setText(device.getModel().toDisplayString()); mainLabel.setText(device.getModel().toDisplayString());
mainLabel.getStyleClass().add("devicelist-main-label"); mainLabel.getStyleClass().add("main-label");
labelsBox.getChildren().add(mainLabel); labelsBox.getChildren().add(mainLabel);
this.statusLabel = new Label(); this.statusLabel = new Label();
statusLabel.textProperty().bind(status); statusLabel.textProperty().bind(status);
labelsBox.getChildren().add(statusLabel); labelsBox.getChildren().add(statusLabel);
statusLabel.getStyleClass().add("devicelist-status-label"); statusLabel.getStyleClass().add("status-label");
listItem.getChildren().add(labelsBox); listItem.getChildren().add(labelsBox);
HBox.setHgrow(labelsBox, Priority.ALWAYS); HBox.setHgrow(labelsBox, Priority.ALWAYS);

View file

@ -0,0 +1,153 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.external.KeystoreFileImport;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.HyperlinkLabel;
import java.io.*;
public class KeystoreFileImportPane extends TitledPane {
private final KeystoreImportAccordion importAccordion;
private final Wallet wallet;
private final KeystoreFileImport importer;
private Label mainLabel;
private HyperlinkLabel descriptionLabel;
public KeystoreFileImportPane(KeystoreImportAccordion importAccordion, Wallet wallet, KeystoreFileImport importer) {
this.importAccordion = importAccordion;
this.wallet = wallet;
this.importer = importer;
setPadding(Insets.EMPTY);
setGraphic(getTitle());
getStyleClass().add("importpane");
setContent(getContentBox(importer.getKeystoreImportDescription()));
Platform.runLater(() -> {
Node arrow = this.lookup(".arrow");
if(arrow != null) {
arrow.setVisible(false);
arrow.setManaged(false);
}
});
}
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/" + importer.getWalletModel().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(importer.getName());
mainLabel.getStyleClass().add("main-label");
labelsBox.getChildren().add(mainLabel);
this.descriptionLabel = new HyperlinkLabel();
labelsBox.getChildren().add(descriptionLabel);
descriptionLabel.getStyleClass().add("description-label");
descriptionLabel.setText("Keystore file import [View Details...]");
descriptionLabel.setOnAction(event -> {
setExpanded(true);
});
listItem.getChildren().add(labelsBox);
HBox.setHgrow(labelsBox, Priority.ALWAYS);
HBox buttonBox = new HBox();
buttonBox.setAlignment(Pos.CENTER_RIGHT);
Button importButton = new Button("Import File...");
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importFile();
});
buttonBox.getChildren().add(importButton);
listItem.getChildren().add(buttonBox);
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
listItem.setPrefWidth(newValue.getWidth());
});
return listItem;
}
private void importFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " keystore");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", "*.*"),
new FileChooser.ExtensionFilter("JSON", "*.json")
);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
importFile(file);
}
}
private void importFile(File file) {
if(file.exists()) {
try {
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream);
EventManager.get().post(new KeystoreImportEvent(keystore));
} catch (Exception e) {
setExpanded(false);
descriptionLabel.getStyleClass().remove("description-label");
descriptionLabel.getStyleClass().add("description-error");
descriptionLabel.setText("Error Importing [View Details...]");
setContent(getContentBox(e.getMessage()));
}
}
}
private Node getContentBox(String message) {
Label details = new Label(message);
details.setWrapText(true);
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.getChildren().add(details);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}

View file

@ -0,0 +1,32 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.external.KeystoreFileImport;
import com.sparrowwallet.sparrow.external.KeystoreImport;
import com.sparrowwallet.sparrow.external.KeystoreMnemonicImport;
import javafx.collections.ObservableList;
import javafx.scene.control.Accordion;
import java.util.List;
public class KeystoreImportAccordion extends Accordion {
private List<KeystoreImport> importers;
public void setKeystoreImporters(Wallet wallet, ObservableList<KeystoreImport> importers) {
this.importers = importers;
for(KeystoreImport importer : importers) {
KeystoreFileImportPane importPane = null;
if(importer instanceof KeystoreFileImport) {
importPane = new KeystoreFileImportPane(this, wallet, (KeystoreFileImport)importer);
} else if(importer instanceof KeystoreMnemonicImport) {
//TODO:
} else {
throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass());
}
this.getPanes().add(importPane);
}
}
}

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.storage.Storage; import com.sparrowwallet.sparrow.storage.Storage;
import java.io.*; import java.io.*;
@ -17,19 +18,24 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
public class ColdcardMultisig implements MultisigWalletImport, KeystoreImport, WalletExport { public class ColdcardMultisig implements MultisigWalletImport, KeystoreFileImport, WalletExport {
private final Gson gson = new Gson(); private final Gson gson = new Gson();
@Override @Override
public String getName() { public String getName() {
return "Coldcard (Multisig)"; return "Coldcard Multisig";
} }
@Override @Override
public PolicyType getPolicyType() { public PolicyType getKeystorePolicyType() {
return PolicyType.MULTI; return PolicyType.MULTI;
} }
@Override
public WalletModel getWalletModel() {
return WalletModel.COLDCARD;
}
@Override @Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException { public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream); InputStreamReader reader = new InputStreamReader(inputStream);

View file

@ -9,6 +9,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -16,7 +17,7 @@ import java.util.List;
import static com.sparrowwallet.drongo.protocol.ScriptType.*; import static com.sparrowwallet.drongo.protocol.ScriptType.*;
public class ColdcardSinglesig implements SinglesigWalletImport { public class ColdcardSinglesig implements KeystoreFileImport, SinglesigWalletImport {
public static final List<ScriptType> ALLOWED_SCRIPT_TYPES = List.of(P2PKH, P2SH_P2WPKH, P2WPKH); public static final List<ScriptType> ALLOWED_SCRIPT_TYPES = List.of(P2PKH, P2SH_P2WPKH, P2WPKH);
@Override @Override
@ -25,7 +26,29 @@ public class ColdcardSinglesig implements SinglesigWalletImport {
} }
@Override @Override
public Wallet importWallet(InputStream inputStream, ScriptType scriptType) throws ImportException { public PolicyType getKeystorePolicyType() {
return PolicyType.SINGLE;
}
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Advanced > Dump Summary feature on your Coldcard";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.COLDCARD;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException {
Wallet wallet = importWallet(scriptType, inputStream);
return wallet.getKeystores().get(0);
}
@Override
public Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException {
if(!ALLOWED_SCRIPT_TYPES.contains(scriptType)) { if(!ALLOWED_SCRIPT_TYPES.contains(scriptType)) {
throw new ImportException("Script type of " + scriptType + " is not allowed"); throw new ImportException("Script type of " + scriptType + " is not allowed");
} }

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.*; import java.io.*;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@ -17,12 +18,43 @@ import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
public class Electrum implements SinglesigWalletImport, MultisigWalletImport, WalletExport { public class Electrum implements KeystoreFileImport, SinglesigWalletImport, MultisigWalletImport, WalletExport {
@Override @Override
public String getName() { public String getName() {
return "Electrum"; return "Electrum";
} }
@Override
public PolicyType getKeystorePolicyType() {
return PolicyType.SINGLE;
}
@Override
public WalletModel getWalletModel() {
return WalletModel.ELECTRUM;
}
@Override
public String getKeystoreImportDescription() {
return "Import from an Electrum wallet";
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException {
Wallet wallet = importWallet(inputStream);
if(!wallet.getPolicyType().equals(PolicyType.SINGLE) || wallet.getKeystores().size() != 1) {
throw new ImportException("Multisig wallet detected - import it through the File > Import menu");
}
if(!wallet.getScriptType().equals(scriptType)) {
//TODO: Derive appropriate ScriptType keystore
throw new ImportException("Wallet has an incompatible script type of " + wallet.getScriptType());
}
return wallet.getKeystores().get(0);
}
@Override @Override
public Wallet importWallet(InputStream inputStream) throws ImportException { public Wallet importWallet(InputStream inputStream) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream); InputStreamReader reader = new InputStreamReader(inputStream);
@ -89,7 +121,7 @@ public class Electrum implements SinglesigWalletImport, MultisigWalletImport, Wa
} }
@Override @Override
public Wallet importWallet(InputStream inputStream, ScriptType scriptType) throws ImportException { public Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException {
Wallet wallet = importWallet(inputStream); Wallet wallet = importWallet(inputStream);
wallet.setScriptType(scriptType); wallet.setScriptType(scriptType);

View file

@ -23,29 +23,9 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
public class Hwi implements KeystoreImport { public class Hwi {
private static File hwiExecutable; 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 { public List<Device> enumerate(String passphrase) throws ImportException {
try { try {
List<String> command; List<String> command;

View file

@ -0,0 +1,10 @@
package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.io.InputStream;
public interface KeystoreFileImport extends KeystoreImport {
Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException;
}

View file

@ -1,13 +1,10 @@
package com.sparrowwallet.sparrow.external; package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.io.InputStream;
public interface KeystoreImport extends Import { public interface KeystoreImport extends Import {
PolicyType getPolicyType(); PolicyType getKeystorePolicyType();
Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException; WalletModel getWalletModel();
String getKeystoreImportDescription(); String getKeystoreImportDescription();
} }

View file

@ -0,0 +1,8 @@
package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
public interface KeystoreMnemonicImport extends KeystoreImport {
Keystore getKeystore(ScriptType scriptType, String[] mnemonicWords, String passphrase) throws ImportException;
}

View file

@ -7,5 +7,5 @@ import java.io.InputStream;
public interface SinglesigWalletImport extends Import { public interface SinglesigWalletImport extends Import {
String getWalletImportDescription(); String getWalletImportDescription();
Wallet importWallet(InputStream inputStream, ScriptType scriptType) throws ImportException; Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException;
} }

View file

@ -63,6 +63,7 @@ public class KeystoreImportController implements Initializable {
Node importTypeNode = importLoader.load(); Node importTypeNode = importLoader.load();
KeystoreImportDetailController controller = importLoader.getController(); KeystoreImportDetailController controller = importLoader.getController();
controller.setMasterController(this); controller.setMasterController(this);
controller.initializeView();
importPane.getChildren().add(importTypeNode); importPane.getChildren().add(importTypeNode);
return importLoader; return importLoader;

View file

@ -10,4 +10,8 @@ public abstract class KeystoreImportDetailController {
void setMasterController(KeystoreImportController masterController) { void setMasterController(KeystoreImportController masterController) {
this.masterController = masterController; this.masterController = masterController;
} }
public void initializeView() {
}
} }

View file

@ -43,6 +43,7 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
@Subscribe @Subscribe
public void keystoreImported(KeystoreImportEvent event) { public void keystoreImported(KeystoreImportEvent event) {
this.keystore = event.getKeystore(); this.keystore = event.getKeystore();
System.out.println(keystore.getLabel() + " " + keystore.getKeyDerivation().getMasterFingerprint());
this.close(); this.close();
} }
} }

View file

@ -0,0 +1,28 @@
package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.sparrow.control.KeystoreImportAccordion;
import com.sparrowwallet.sparrow.external.ColdcardMultisig;
import com.sparrowwallet.sparrow.external.ColdcardSinglesig;
import com.sparrowwallet.sparrow.external.KeystoreImport;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import java.util.Collections;
import java.util.List;
public class UsbAirgappedController extends KeystoreImportDetailController {
@FXML
private KeystoreImportAccordion importAccordion;
public void initializeView() {
List<KeystoreImport> importers = Collections.emptyList();
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
importers = List.of(new ColdcardSinglesig());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
importers = List.of(new ColdcardMultisig());
}
importAccordion.setKeystoreImporters(getMasterController().getWallet(), FXCollections.observableList(importers));
}
}

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 com.sparrowwallet.sparrow.control.KeystoreImportAccordion?>
<AnchorPane stylesheets="@keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.UsbAirgappedController">
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true">
<KeystoreImportAccordion fx:id="importAccordion" />
</ScrollPane>
</AnchorPane>

View file

@ -47,14 +47,19 @@
-fx-translate-x: -1000; -fx-translate-x: -1000;
} }
.devicelist-main-label .text { .main-label .text {
} }
.devicelist-status-label .text { .status-label .text, .description-label Text {
-fx-fill: #a0a1a7; -fx-fill: #a0a1a7;
} }
.status-error .text { .status-error .text, .description-error Text {
-fx-fill: #ca1243; -fx-fill: #ca1243;
} }
.description-label .text, description-error .text {
-fx-fill: #1e88cf;
}