diff --git a/drongo b/drongo index 50fdb71b..caed93ca 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 50fdb71bf3e67db55b2cd66dc3fbdc8faf73be63 +Subproject commit caed93ca6d3e29433c13ed9db705e34e057caf43 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java index 73ca49a0..658777bc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java @@ -215,7 +215,7 @@ public abstract class FileImportPane extends TitledDescriptionPane { private Node getPasswordEntry(File file) { CustomPasswordField passwordField = new ViewPasswordField(); - passwordField.setPromptText("Wallet password"); + passwordField.setPromptText("Password"); password.bind(passwordField.textProperty()); HBox.setHgrow(passwordField, Priority.ALWAYS); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java new file mode 100644 index 00000000..f97ae4b5 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java @@ -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 operationFingerprints = List.of(keystore.getKeyDerivation().getMasterFingerprint()); + + DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(operationFingerprints, wallet, message, keystore.getKeyDerivation()); + Optional 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 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); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreExportDialog.java new file mode 100644 index 00000000..8b70818d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreExportDialog.java @@ -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 { + 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 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()); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/KeystoreExportEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/KeystoreExportEvent.java new file mode 100644 index 00000000..ed864b56 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/KeystoreExportEvent.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java b/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java new file mode 100644 index 00000000..8317b80e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java @@ -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 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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 53ce5236..766d17eb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -48,6 +48,7 @@ public class Config { private boolean showLoadingLog = true; private boolean showAddressTransactionCount = false; private boolean showDeprecatedImportExport = false; + private boolean signBsmsExports = false; private boolean preventSleep = false; private List recentWalletFiles; private Integer keyDerivationPeriod; @@ -315,6 +316,15 @@ public class Config { flush(); } + public boolean isSignBsmsExports() { + return signBsmsExports; + } + + public void setSignBsmsExports(boolean signBsmsExports) { + this.signBsmsExports = signBsmsExports; + flush(); + } + public boolean isPreventSleep() { return preventSleep; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index a9f7ff53..f48284d2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -301,19 +301,28 @@ public class Hwi { Process process = null; try { List processArguments = new ArrayList<>(arguments); - processArguments.add("--stdin"); + + boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n")); + if(useStdin) { + processArguments.add("--stdin"); + } else { + processArguments.add(command.toString()); + processArguments.addAll(Arrays.asList(commandArguments)); + } ProcessBuilder processBuilder = new ProcessBuilder(processArguments); process = processBuilder.start(); - try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { - writer.write(command.toString()); - for(String commandArgument : commandArguments) { - writer.write(" \""); - writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\"")); - writer.write("\""); + if(useStdin) { + try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(command.toString()); + for(String commandArgument : commandArguments) { + writer.write(" \""); + writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\"")); + writer.write("\""); + } + writer.flush(); } - writer.flush(); } return getProcessOutput(process); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoreExport.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreExport.java new file mode 100644 index 00000000..f2c5ad4d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreExport.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.io; + +public interface KeystoreExport extends ImportExport { + String getKeystoreExportDescription(); +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoreFileExport.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreFileExport.java new file mode 100644 index 00000000..35b1beda --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreFileExport.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java index 2cf66a51..653db879 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java @@ -23,10 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.smartcardio.CardException; -import java.util.Base64; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; public class CkCardApi extends CardApi { 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 derivation) throws CardException { - List keystoreDerivation = derivation.subList(0, derivation.size() - 2); - List subPathDerivation = derivation.subList(derivation.size() - 2, derivation.size()); + List keystoreDerivation; + List subPathDerivation; + + Optional 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(); KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation(); @@ -239,9 +246,15 @@ public class CkCardApi extends CardApi { signingKeystore = getKeystore(); } - WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation)); - ECKey addressPubKey = signingKeystore.getPubKey(addressNode); - return addressPubKey.signMessage(message, scriptType, hash -> { + ECKey signingPubKey; + if(subPathDerivation.isEmpty()) { + signingPubKey = signingKeystore.getExtendedPublicKey().getKey(); + } else { + WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation)); + signingPubKey = signingKeystore.getPubKey(addressNode); + } + + return signingPubKey.signMessage(message, scriptType, hash -> { try { CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash); return cardSign.getSignature(); diff --git a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java index 9bf251b6..7fa8e2da 100644 --- a/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java +++ b/src/main/java/com/sparrowwallet/sparrow/keystoreimport/HwAirgappedController.java @@ -28,7 +28,7 @@ public class HwAirgappedController extends KeystoreImportDetailController { 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()); } 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) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index 5a26edc5..d147c766 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.wallet; import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.*; +import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; @@ -56,6 +57,9 @@ public class KeystoreController extends WalletFormController implements Initiali @FXML private Label type; + @FXML + private Button exportButton; + @FXML private Button viewSeedButton; @@ -129,6 +133,7 @@ public class KeystoreController extends WalletFormController implements Initiali } } + exportButton.managedProperty().bind(exportButton.visibleProperty()); viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty()); viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty()); changePinButton.managedProperty().bind(changePinButton.visibleProperty()); @@ -136,7 +141,7 @@ public class KeystoreController extends WalletFormController implements Initiali displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty()); displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not()); - updateType(); + updateType(false); 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.setGraphic(getTypeIcon(keystore)); + exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI); viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed()); viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey()); changePinButton.setVisible(keystore.getWalletModel().isCard()); @@ -309,9 +315,9 @@ public class KeystoreController extends WalletFormController implements Initiali private String getTypeLabel(Keystore keystore) { switch (keystore.getSource()) { case HW_USB: - return "Connected Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")"; + return "Connected Wallet (" + keystore.getWalletModel().toDisplayString() + ")"; case HW_AIRGAPPED: - return "Airgapped Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")"; + return "Airgapped Wallet (" + keystore.getWalletModel().toDisplayString() + ")"; case SW_SEED: return "Software Wallet"; case SW_WATCH: @@ -367,7 +373,7 @@ public class KeystoreController extends WalletFormController implements Initiali keystore.setSeed(importedKeystore.getSeed()); keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey()); - updateType(); + updateType(true); label.setText(keystore.getLabel()); fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint()); 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) { int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore); 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); + } + } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml index f157d981..67c26fd2 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/keystore.fxml @@ -24,30 +24,37 @@