From 6e10784e494d0575c3a98dab73d546af75ce5600 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 3 Sep 2020 20:46:44 +0200 Subject: [PATCH] support usb message signing --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 3 +- .../sparrow/control/DevicePane.java | 67 ++++++++++++++++++- .../control/DeviceSignMessageDialog.java | 39 +++++++++++ .../sparrow/control/EntryCell.java | 4 +- .../sparrow/control/MessageSignDialog.java | 31 +++++++-- .../sparrow/event/MessageSignedEvent.java | 25 +++++++ .../com/sparrowwallet/sparrow/io/Hwi.java | 58 +++++++++++++++- .../sparrow/io/SignMessageException.java | 19 ++++++ 9 files changed, 236 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/DeviceSignMessageDialog.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/MessageSignedEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/SignMessageException.java diff --git a/drongo b/drongo index ee49ddd9..10ebfe46 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit ee49ddd94bdfec1e87d334f17833a63382958790 +Subproject commit 10ebfe463d504f39be2aacec41d48c2cf5ba4c56 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 6f9de6e9..efb6089b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -854,7 +854,8 @@ public class AppController implements Initializable { if(tab != null && tab.getUserData() instanceof WalletTabData) { WalletTabData walletTabData = (WalletTabData)tab.getUserData(); Wallet wallet = walletTabData.getWallet(); - if(wallet.getKeystores().size() == 1 && wallet.getKeystores().get(0).hasSeed()) { + if(wallet.getKeystores().size() == 1 && + (wallet.getKeystores().get(0).hasSeed() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) { //Can sign and verify messageSignDialog = new MessageSignDialog(wallet); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index ee022b1a..3074d8b2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.AddressDisplayedEvent; import com.sparrowwallet.sparrow.event.KeystoreImportEvent; +import com.sparrowwallet.sparrow.event.MessageSignedEvent; import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Hwi; @@ -38,6 +39,7 @@ public class DevicePane extends TitledDescriptionPane { private final Wallet wallet; private final PSBT psbt; private final KeyDerivation keyDerivation; + private final String message; private final Device device; private CustomPasswordField pinField; @@ -47,6 +49,7 @@ public class DevicePane extends TitledDescriptionPane { private SplitMenuButton importButton; private Button signButton; private Button displayAddressButton; + private Button signMessageButton; private final SimpleStringProperty passphrase = new SimpleStringProperty(""); @@ -56,6 +59,7 @@ public class DevicePane extends TitledDescriptionPane { this.wallet = wallet; this.psbt = null; this.keyDerivation = null; + this.message = null; this.device = device; setDefaultStatus(); @@ -81,6 +85,7 @@ public class DevicePane extends TitledDescriptionPane { this.wallet = null; this.psbt = psbt; this.keyDerivation = null; + this.message = null; this.device = device; setDefaultStatus(); @@ -106,6 +111,7 @@ public class DevicePane extends TitledDescriptionPane { this.wallet = wallet; this.psbt = null; this.keyDerivation = keyDerivation; + this.message = null; this.device = device; setDefaultStatus(); @@ -125,6 +131,32 @@ public class DevicePane extends TitledDescriptionPane { buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton); } + public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device) { + super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); + this.deviceOperation = DeviceOperation.SIGN_MESSAGE; + this.wallet = wallet; + this.psbt = null; + this.keyDerivation = keyDerivation; + this.message = message; + this.device = device; + + setDefaultStatus(); + showHideLink.setVisible(false); + + createSetPassphraseButton(); + createSignMessageButton(); + + if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) { + unlockButton.setVisible(true); + } else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) { + setPassphraseButton.setVisible(true); + } else { + showOperationButton(); + } + + buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton); + } + @Override protected Control createButton() { createUnlockButton(); @@ -209,6 +241,22 @@ public class DevicePane extends TitledDescriptionPane { } } + private void createSignMessageButton() { + signMessageButton = new Button("Sign Message"); + signMessageButton.setDefaultButton(true); + signMessageButton.setAlignment(Pos.CENTER_RIGHT); + signMessageButton.setOnAction(event -> { + signMessageButton.setDisable(true); + signMessage(); + }); + signMessageButton.managedProperty().bind(signMessageButton.visibleProperty()); + signMessageButton.setVisible(false); + + if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) { + signMessageButton.setDisable(true); + } + } + private void unlock(Device device) { if(device.getModel().equals(WalletModel.TREZOR_1)) { promptPin(); @@ -438,6 +486,20 @@ public class DevicePane extends TitledDescriptionPane { displayAddressService.start(); } + private void signMessage() { + Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath()); + signMessageService.setOnSucceeded(successEvent -> { + String signature = signMessageService.getValue(); + EventManager.get().post(new MessageSignedEvent(wallet, signature)); + }); + signMessageService.setOnFailed(failedEvent -> { + setError("Could not sign message", signMessageService.getException().getMessage()); + signMessageButton.setDisable(false); + }); + setDescription("Signing message..."); + signMessageService.start(); + } + private void showOperationButton() { if(deviceOperation.equals(DeviceOperation.IMPORT)) { importButton.setVisible(true); @@ -450,6 +512,9 @@ public class DevicePane extends TitledDescriptionPane { } else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) { displayAddressButton.setVisible(true); showHideLink.setVisible(false); + } else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) { + signMessageButton.setVisible(true); + showHideLink.setVisible(false); } } @@ -490,6 +555,6 @@ public class DevicePane extends TitledDescriptionPane { } public enum DeviceOperation { - IMPORT, SIGN, DISPLAY_ADDRESS; + IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignMessageDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignMessageDialog.java new file mode 100644 index 00000000..2a1a630d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignMessageDialog.java @@ -0,0 +1,39 @@ +package com.sparrowwallet.sparrow.control; + +import com.google.common.eventbus.Subscribe; +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.MessageSignedEvent; +import com.sparrowwallet.sparrow.io.Device; + +import java.util.List; + +public class DeviceSignMessageDialog extends DeviceDialog { + private final Wallet wallet; + private final String message; + private final KeyDerivation keyDerivation; + + public DeviceSignMessageDialog(List operationFingerprints, Wallet wallet, String message, KeyDerivation keyDerivation) { + super(operationFingerprints); + this.wallet = wallet; + this.message = message; + this.keyDerivation = keyDerivation; + EventManager.get().register(this); + setOnCloseRequest(event -> { + EventManager.get().unregister(this); + }); + } + + @Override + protected DevicePane getDevicePane(Device device) { + return new DevicePane(wallet, message, keyDerivation, device); + } + + @Subscribe + public void messageSigned(MessageSignedEvent event) { + if(wallet == event.getWallet()) { + setResult(event.getSignature()); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index ca26120e..5c04fd89 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; @@ -88,7 +89,8 @@ class EntryCell extends TreeTableCell { }); actionBox.getChildren().add(receiveButton); - if(nodeEntry.getWallet().getKeystores().size() == 1 && nodeEntry.getWallet().getKeystores().get(0).hasSeed()) { + if(nodeEntry.getWallet().getKeystores().size() == 1 && + (nodeEntry.getWallet().getKeystores().get(0).hasSeed() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) { Button signMessageButton = new Button(""); Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY); signMessageGlyph.setFontSize(12); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java index 07a7913a..7d71ccd4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.control; import com.google.common.eventbus.Subscribe; +import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.InvalidAddressException; @@ -8,6 +9,7 @@ import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.policy.PolicyType; 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.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppController; @@ -32,7 +34,7 @@ import tornadofx.control.Fieldset; import tornadofx.control.Form; import java.security.SignatureException; -import java.util.Arrays; +import java.util.List; import java.util.Optional; import static com.sparrowwallet.sparrow.AppController.setStageIcon; @@ -73,8 +75,8 @@ public class MessageSignDialog extends Dialog { if(wallet.getKeystores().size() != 1) { throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required"); } - if(!wallet.getKeystores().get(0).hasSeed()) { - throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed"); + if(!wallet.getKeystores().get(0).hasSeed() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) { + throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore"); } } @@ -211,10 +213,14 @@ public class MessageSignDialog extends Dialog { return; } - if(wallet.isEncrypted()) { - EventManager.get().post(new RequestOpenWalletsEvent()); - } else { - signUnencryptedKeystore(wallet); + if(wallet.containsSeeds()) { + if(wallet.isEncrypted()) { + EventManager.get().post(new RequestOpenWalletsEvent()); + } else { + signUnencryptedKeystore(wallet); + } + } else if(wallet.containsSource(KeystoreSource.HW_USB)) { + signUsbKeystore(wallet); } } @@ -232,6 +238,17 @@ public class MessageSignDialog extends Dialog { } } + private void signUsbKeystore(Wallet usbWallet) { + List fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); + KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation()); + DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation); + Optional optSignature = deviceSignMessageDialog.showAndWait(); + if(optSignature.isPresent()) { + signature.clear(); + signature.appendText(optSignature.get()); + } + } + private void verifyMessage() { try { //Find ECKey from message and signature diff --git a/src/main/java/com/sparrowwallet/sparrow/event/MessageSignedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/MessageSignedEvent.java new file mode 100644 index 00000000..e7e0c975 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/MessageSignedEvent.java @@ -0,0 +1,25 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; + +/** + * This event is used by the DeviceSignMessageDialog to indicate that a USB device has signed a message + * + */ +public class MessageSignedEvent { + private final Wallet wallet; + private final String signature; + + public MessageSignedEvent(Wallet wallet, String signature) { + this.wallet = wallet; + this.signature = signature; + } + + public Wallet getWallet() { + return wallet; + } + + public String getSignature() { + return signature; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index 913055e2..ca5eaeb2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -116,13 +116,44 @@ public class Hwi { if(result.get("address") != null) { return result.get("address").getAsString(); } else { - throw new DisplayAddressException("Could not retrieve address"); + JsonElement error = result.get("error"); + if(error != null) { + throw new DisplayAddressException(error.getAsString()); + } else { + throw new DisplayAddressException("Could not retrieve address"); + } } } catch(IOException e) { throw new DisplayAddressException(e); } } + public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException { + try { + isPromptActive = false; + String output; + if(passphrase != null && !passphrase.isEmpty() && device.getModel().equals(WalletModel.TREZOR_1)) { + output = execute(getDeviceCommand(device, passphrase, Command.SIGN_MESSAGE, message, derivationPath)); + } else { + output = execute(getDeviceCommand(device, Command.SIGN_MESSAGE, message, derivationPath)); + } + + JsonObject result = JsonParser.parseString(output).getAsJsonObject(); + if(result.get("signature") != null) { + return result.get("signature").getAsString(); + } else { + JsonElement error = result.get("error"); + if(error != null) { + throw new SignMessageException(error.getAsString()); + } else { + throw new SignMessageException("Could not sign message"); + } + } + } catch(IOException e) { + throw new SignMessageException(e); + } + } + public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException { try { String psbtBase64 = psbt.toBase64String(); @@ -409,6 +440,30 @@ public class Hwi { } } + public static class SignMessageService extends Service { + private final Device device; + private final String passphrase; + private final String message; + private final String derivationPath; + + public SignMessageService(Device device, String passphrase, String message, String derivationPath) { + this.device = device; + this.passphrase = passphrase; + this.message = message; + this.derivationPath = derivationPath; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected String call() throws SignMessageException { + Hwi hwi = new Hwi(); + return hwi.signMessage(device, passphrase, message, derivationPath); + } + }; + } + } + public static class GetXpubService extends Service { private final Device device; private final String passphrase; @@ -479,6 +534,7 @@ public class Hwi { PROMPT_PIN("promptpin", true), SEND_PIN("sendpin", false), DISPLAY_ADDRESS("displayaddress", true), + SIGN_MESSAGE("signmessage", true), GET_XPUB("getxpub", true), SIGN_TX("signtx", true); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/SignMessageException.java b/src/main/java/com/sparrowwallet/sparrow/io/SignMessageException.java new file mode 100644 index 00000000..ac88f3ff --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/SignMessageException.java @@ -0,0 +1,19 @@ +package com.sparrowwallet.sparrow.io; + +public class SignMessageException extends Exception { + public SignMessageException() { + super(); + } + + public SignMessageException(String message) { + super(message); + } + + public SignMessageException(Throwable cause) { + super(cause); + } + + public SignMessageException(String message, Throwable cause) { + super(message, cause); + } +}