bip129 round 1 support with optional signing of bsms keystore exports

This commit is contained in:
Craig Raw 2023-02-22 10:22:04 +02:00
parent 1cb6778502
commit 2a7f14a4ed
14 changed files with 565 additions and 49 deletions

2
drongo

@ -1 +1 @@
Subproject commit 50fdb71bf3e67db55b2cd66dc3fbdc8faf73be63 Subproject commit caed93ca6d3e29433c13ed9db705e34e057caf43

View file

@ -215,7 +215,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
private Node getPasswordEntry(File file) { private Node getPasswordEntry(File file) {
CustomPasswordField passwordField = new ViewPasswordField(); CustomPasswordField passwordField = new ViewPasswordField();
passwordField.setPromptText("Wallet password"); passwordField.setPromptText("Password");
password.bind(passwordField.textProperty()); password.bind(passwordField.textProperty());
HBox.setHgrow(passwordField, Priority.ALWAYS); HBox.setHgrow(passwordField, Priority.ALWAYS);

View file

@ -0,0 +1,168 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
public class FileKeystoreExportPane extends TitledDescriptionPane {
private final Keystore keystore;
private final KeystoreFileExport exporter;
private final boolean scannable;
private final boolean file;
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.keystore = keystore;
this.exporter = exporter;
this.scannable = exporter.isKeystoreExportScannable();
this.file = exporter.isKeystoreExportFile();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
if(scannable && file) {
ToggleButton showButton = new ToggleButton("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
showButton.setSelected(false);
exportQR();
});
ToggleButton fileButton = new ToggleButton("Export File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
exportFile();
});
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(showButton, fileButton);
return segmentedButton;
} else if(scannable) {
Button showButton = new Button("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
exportQR();
});
return showButton;
} else {
Button exportButton = new Button("Export File...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportFile();
});
return exportButton;
}
}
private void exportQR() {
exportKeystore(null, keystore);
}
private void exportFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(keystore);
String fileName = keystore.getLabel();
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
exportKeystore(file, keystore);
}
}
private void exportKeystore(File file, Keystore exportKeystore) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
exporter.exportKeystore(exportKeystore, baos);
if(exporter.requiresSignature()) {
String message = baos.toString(StandardCharsets.UTF_8);
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getWalletModel().isCard()) {
TextAreaDialog dialog = new TextAreaDialog(message, false);
dialog.setTitle("Sign " + exporter.getName() + " Export");
dialog.getDialogPane().setHeaderText("The following text needs to be signed by the device.\nClick OK to continue.");
dialog.showAndWait();
Wallet wallet = new Wallet();
wallet.setScriptType(ScriptType.P2PKH);
wallet.getKeystores().add(keystore);
List<String> operationFingerprints = List.of(keystore.getKeyDerivation().getMasterFingerprint());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(operationFingerprints, wallet, message, keystore.getKeyDerivation());
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
exporter.addSignature(keystore, optSignature.get(), baos);
}
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
String signature = keystore.getExtendedPrivateKey().getKey().signMessage(message, ScriptType.P2PKH);
exporter.addSignature(keystore, signature, baos);
} else {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Cannot sign export",
"Signing the " + exporter.getName() + " export with " + keystore.getWalletModel().toDisplayString() + " is not supported." +
"Proceed without signing?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.NO) {
throw new RuntimeException("Export aborted due to lack of device message signing support.");
}
}
}
if(file != null) {
try(OutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(baos.toByteArray());
EventManager.get().post(new KeystoreExportEvent(exportKeystore));
}
} else {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
}
qrDisplayDialog.showAndWait();
}
} catch(Exception e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
}
}
}

View file

@ -0,0 +1,66 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.io.*;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.Comparator;
import java.util.List;
public class KeystoreExportDialog extends Dialog<Keystore> {
public KeystoreExportDialog(Keystore keystore) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(200);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<KeystoreFileExport> exporters = List.of(new Bip129());
Accordion exportAccordion = new Accordion();
for(KeystoreFileExport exporter : exporters) {
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileKeystoreExportPane exportPane = new FileKeystoreExportPane(keystore, exporter);
exportAccordion.getPanes().add(exportPane);
}
}
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
scrollPane.setContent(exportAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(280);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
}
@Subscribe
public void keystoreExported(KeystoreExportEvent event) {
setResult(event.getKeystore());
}
}

View file

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

View file

@ -0,0 +1,189 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.io.CharStreams;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.*;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.List;
public class Bip129 implements KeystoreFileExport, KeystoreFileImport {
@Override
public String getName() {
return "BSMS";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SEED;
}
@Override
public String getKeystoreExportDescription() {
return "Exports the keystore in the Bitcoin Secure Multisig Setup (BSMS) format.";
}
@Override
public void exportKeystore(Keystore keystore, OutputStream outputStream) throws ExportException {
if(!keystore.isValid()) {
throw new ExportException("Invalid keystore");
}
try {
String record = "BSMS 1.0\n00\n[" +
keystore.getKeyDerivation().toString() +
"]" +
keystore.getExtendedPublicKey().toString() +
"\n" +
keystore.getLabel();
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
} catch(Exception e) {
throw new ExportException("Error writing BSMS file", e);
}
}
@Override
public boolean requiresSignature() {
//Due to poor vendor support of multiline message signing at the xpub derivation path, signing BSMS keystore exports is configurable (default false)
return Config.get().isSignBsmsExports();
}
@Override
public void addSignature(Keystore keystore, String signature, OutputStream outputStream) throws ExportException {
try {
String append = "\n" + signature;
outputStream.write(append.getBytes(StandardCharsets.UTF_8));
} catch(Exception e) {
throw new ExportException("Error writing BSMS file", e);
}
}
@Override
public String getExportFileExtension(Keystore keystore) {
return "bsms";
}
@Override
public boolean isKeystoreExportScannable() {
return true;
}
@Override
public boolean isEncrypted(File file) {
try {
try(BufferedReader reader = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) {
String text = CharStreams.toString(reader);
return Utils.isHex(text.trim());
}
} catch(Exception e) {
return false;
}
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
try(BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
BufferedReader reader = streamReader;
if(password != null) {
byte[] token;
if((password.length() == 16 || password.length() == 32) && Utils.isHex(password)) {
token = Utils.hexToBytes(password);
} else if(Utils.isNumber(password)) {
BigInteger bi = new BigInteger(password);
token = Utils.bigIntegerToBytes(bi, bi.toByteArray().length >= 16 ? 16 : 8);
} else if(password.split(" ").length == 6 || password.split(" ").length == 12) {
List<String> mnemonicWords = Arrays.asList(password.split(" "));
token = Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicWords);
} else {
throw new ImportException("Provided password needs to be in hexadecimal, decimal or mnemonic format.");
}
String hex = CharStreams.toString(streamReader).trim();
byte[] data = Utils.hexToBytes(hex);
byte[] mac = Arrays.copyOfRange(data, 0, 32);
byte[] iv = Arrays.copyOfRange(mac, 0, 16);
byte[] ciphertext = Arrays.copyOfRange(data, 32, data.length);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
Pbkdf2KeyDeriver pbkdf2KeyDeriver = new Pbkdf2KeyDeriver(token, 2048, 256);
byte[] key = pbkdf2KeyDeriver.deriveKey("No SPOF").getKeyBytes();
Key keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] plaintext = cipher.doFinal(ciphertext);
String plaintextString = new String(plaintext, StandardCharsets.UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(Sha256Hash.hash(key), "HmacSHA256");
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(secretKeySpec);
String macData = Utils.bytesToHex(token) + plaintextString;
byte[] calculatedMac = hmac.doFinal(macData.getBytes(StandardCharsets.UTF_8));
if(!Arrays.equals(mac, calculatedMac)) {
throw new ImportException("Message digest authentication failed.");
}
reader = new BufferedReader(new StringReader(plaintextString));
}
String header = reader.readLine();
String token = reader.readLine();
String descriptor = reader.readLine();
String label = reader.readLine();
String signature = reader.readLine();
return getKeystore(header, token, descriptor, label, signature);
}
} catch(MnemonicException.MnemonicWordException e) {
throw new ImportException("Error importing BSMS: Invalid mnemonic word " + e.badWord, e);
} catch(MnemonicException.MnemonicChecksumException e) {
throw new ImportException("Error importing BSMS: Invalid mnemonic checksum", e);
} catch(Exception e) {
throw new ImportException("Error importing BSMS", e);
}
}
private Keystore getKeystore(String header, String token, String descriptor, String label, String signature) throws ImportException {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor("sh(" + descriptor + ")");
Wallet wallet = outputDescriptor.toWallet();
Keystore keystore = wallet.getKeystores().get(0);
keystore.setLabel(label);
if(signature != null) {
try {
String message = header + "\n" + token + "\n" + descriptor + "\n" + label;
keystore.getExtendedPublicKey().getKey().verifyMessage(message, signature);
} catch(SignatureException e) {
throw new ImportException("Signature did not match provided public key", e);
}
}
return keystore;
}
@Override
public String getKeystoreImportDescription(int account) {
return "Imports a keystore that was exported using the Bitcoin Secure Multisig Setup (BSMS) format.";
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
}

View file

@ -48,6 +48,7 @@ public class Config {
private boolean showLoadingLog = true; private boolean showLoadingLog = true;
private boolean showAddressTransactionCount = false; private boolean showAddressTransactionCount = false;
private boolean showDeprecatedImportExport = false; private boolean showDeprecatedImportExport = false;
private boolean signBsmsExports = false;
private boolean preventSleep = false; private boolean preventSleep = false;
private List<File> recentWalletFiles; private List<File> recentWalletFiles;
private Integer keyDerivationPeriod; private Integer keyDerivationPeriod;
@ -315,6 +316,15 @@ public class Config {
flush(); flush();
} }
public boolean isSignBsmsExports() {
return signBsmsExports;
}
public void setSignBsmsExports(boolean signBsmsExports) {
this.signBsmsExports = signBsmsExports;
flush();
}
public boolean isPreventSleep() { public boolean isPreventSleep() {
return preventSleep; return preventSleep;
} }

View file

@ -301,11 +301,19 @@ public class Hwi {
Process process = null; Process process = null;
try { try {
List<String> processArguments = new ArrayList<>(arguments); List<String> processArguments = new ArrayList<>(arguments);
boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n"));
if(useStdin) {
processArguments.add("--stdin"); processArguments.add("--stdin");
} else {
processArguments.add(command.toString());
processArguments.addAll(Arrays.asList(commandArguments));
}
ProcessBuilder processBuilder = new ProcessBuilder(processArguments); ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
process = processBuilder.start(); process = processBuilder.start();
if(useStdin) {
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write(command.toString()); writer.write(command.toString());
for(String commandArgument : commandArguments) { for(String commandArgument : commandArguments) {
@ -315,6 +323,7 @@ public class Hwi {
} }
writer.flush(); writer.flush();
} }
}
return getProcessOutput(process); return getProcessOutput(process);
} finally { } finally {

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.io;
public interface KeystoreExport extends ImportExport {
String getKeystoreExportDescription();
}

View file

@ -0,0 +1,16 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.io.OutputStream;
public interface KeystoreFileExport extends KeystoreExport {
void exportKeystore(Keystore keystore, OutputStream outputStream) throws ExportException;
boolean requiresSignature();
void addSignature(Keystore keystore, String signature, OutputStream outputStream) throws ExportException;
String getExportFileExtension(Keystore keystore);
boolean isKeystoreExportScannable();
default boolean isKeystoreExportFile() {
return true;
}
}

View file

@ -23,10 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException; import javax.smartcardio.CardException;
import java.util.Base64; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class CkCardApi extends CardApi { public class CkCardApi extends CardApi {
private static final Logger log = LoggerFactory.getLogger(CkCardApi.class); private static final Logger log = LoggerFactory.getLogger(CkCardApi.class);
@ -227,8 +224,18 @@ public class CkCardApi extends CardApi {
} }
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException { String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
List<ChildNumber> keystoreDerivation = derivation.subList(0, derivation.size() - 2); List<ChildNumber> keystoreDerivation;
List<ChildNumber> subPathDerivation = derivation.subList(derivation.size() - 2, derivation.size()); List<ChildNumber> subPathDerivation;
Optional<ChildNumber> firstUnhardened = derivation.stream().filter(cn -> !cn.isHardened()).findFirst();
if(firstUnhardened.isPresent()) {
int index = derivation.indexOf(firstUnhardened.get());
keystoreDerivation = derivation.subList(0, index);
subPathDerivation = derivation.subList(index, derivation.size());
} else {
keystoreDerivation = derivation;
subPathDerivation = Collections.emptyList();
}
Keystore cardKeystore = getKeystore(); Keystore cardKeystore = getKeystore();
KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation(); KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation();
@ -239,9 +246,15 @@ public class CkCardApi extends CardApi {
signingKeystore = getKeystore(); signingKeystore = getKeystore();
} }
ECKey signingPubKey;
if(subPathDerivation.isEmpty()) {
signingPubKey = signingKeystore.getExtendedPublicKey().getKey();
} else {
WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation)); WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation));
ECKey addressPubKey = signingKeystore.getPubKey(addressNode); signingPubKey = signingKeystore.getPubKey(addressNode);
return addressPubKey.signMessage(message, scriptType, hash -> { }
return signingPubKey.signMessage(message, scriptType, hash -> {
try { try {
CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash); CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash);
return cardSign.getSignature(); return cardSign.getSignature();

View file

@ -28,7 +28,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) { if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY()); fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) { } else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
fileImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY()); fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
} }
for(KeystoreFileImport importer : fileImporters) { for(KeystoreFileImport importer : fileImporters) {

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.*; import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -56,6 +57,9 @@ public class KeystoreController extends WalletFormController implements Initiali
@FXML @FXML
private Label type; private Label type;
@FXML
private Button exportButton;
@FXML @FXML
private Button viewSeedButton; private Button viewSeedButton;
@ -129,6 +133,7 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
} }
exportButton.managedProperty().bind(exportButton.visibleProperty());
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty()); viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty()); viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty());
changePinButton.managedProperty().bind(changePinButton.visibleProperty()); changePinButton.managedProperty().bind(changePinButton.visibleProperty());
@ -136,7 +141,7 @@ public class KeystoreController extends WalletFormController implements Initiali
displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty()); displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty());
displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not()); displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not());
updateType(); updateType(false);
label.setText(keystore.getLabel()); label.setText(keystore.getLabel());
@ -280,9 +285,10 @@ public class KeystoreController extends WalletFormController implements Initiali
)); ));
} }
private void updateType() { private void updateType(boolean showExport) {
type.setText(getTypeLabel(keystore)); type.setText(getTypeLabel(keystore));
type.setGraphic(getTypeIcon(keystore)); type.setGraphic(getTypeIcon(keystore));
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed()); viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey()); viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
changePinButton.setVisible(keystore.getWalletModel().isCard()); changePinButton.setVisible(keystore.getWalletModel().isCard());
@ -309,9 +315,9 @@ public class KeystoreController extends WalletFormController implements Initiali
private String getTypeLabel(Keystore keystore) { private String getTypeLabel(Keystore keystore) {
switch (keystore.getSource()) { switch (keystore.getSource()) {
case HW_USB: case HW_USB:
return "Connected Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")"; return "Connected Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
case HW_AIRGAPPED: case HW_AIRGAPPED:
return "Airgapped Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")"; return "Airgapped Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
case SW_SEED: case SW_SEED:
return "Software Wallet"; return "Software Wallet";
case SW_WATCH: case SW_WATCH:
@ -367,7 +373,7 @@ public class KeystoreController extends WalletFormController implements Initiali
keystore.setSeed(importedKeystore.getSeed()); keystore.setSeed(importedKeystore.getSeed());
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey()); keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
updateType(); updateType(true);
label.setText(keystore.getLabel()); label.setText(keystore.getLabel());
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint()); fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath()); derivation.setText(keystore.getKeyDerivation().getDerivationPath());
@ -381,6 +387,11 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
} }
public void export(ActionEvent event) {
KeystoreExportDialog keystoreExportDialog = new KeystoreExportDialog(keystore);
keystoreExportDialog.showAndWait();
}
public void showPrivate(ActionEvent event) { public void showPrivate(ActionEvent event) {
int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore); int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore);
Wallet copy = getWalletForm().getWallet().copy(); Wallet copy = getWalletForm().getWallet().copy();
@ -614,4 +625,11 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
} }
} }
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
exportButton.setVisible(false);
}
}
} }

View file

@ -24,6 +24,7 @@
<Fieldset inputGrow="SOMETIMES" text=""> <Fieldset inputGrow="SOMETIMES" text="">
<Field text="Type:"> <Field text="Type:">
<Label fx:id="type" graphicTextGap="5"/> <Label fx:id="type" graphicTextGap="5"/>
<HBox spacing="10" alignment="CENTER_LEFT">
<Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showPrivate"> <Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showPrivate">
<graphic> <graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
@ -48,6 +49,12 @@
<Tooltip text="Change the PIN of current card"/> <Tooltip text="Change the PIN of current card"/>
</tooltip> </tooltip>
</Button> </Button>
<Button fx:id="exportButton" text="Export..." onAction="#export">
<tooltip>
<Tooltip text="Export this keystore as a signer for a multisig wallet"/>
</tooltip>
</Button>
</HBox>
<Pane HBox.hgrow="ALWAYS" /> <Pane HBox.hgrow="ALWAYS" />
<Button fx:id="importButton" text="Import..." graphicTextGap="5" onAction="#importKeystore"> <Button fx:id="importButton" text="Import..." graphicTextGap="5" onAction="#importKeystore">
<graphic> <graphic>