support usb message signing

This commit is contained in:
Craig Raw 2020-09-03 20:46:44 +02:00
parent 885efd7985
commit 6e10784e49
9 changed files with 236 additions and 12 deletions

2
drongo

@ -1 +1 @@
Subproject commit ee49ddd94bdfec1e87d334f17833a63382958790 Subproject commit 10ebfe463d504f39be2aacec41d48c2cf5ba4c56

View file

@ -854,7 +854,8 @@ public class AppController implements Initializable {
if(tab != null && tab.getUserData() instanceof WalletTabData) { if(tab != null && tab.getUserData() instanceof WalletTabData) {
WalletTabData walletTabData = (WalletTabData)tab.getUserData(); WalletTabData walletTabData = (WalletTabData)tab.getUserData();
Wallet wallet = walletTabData.getWallet(); 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 //Can sign and verify
messageSignDialog = new MessageSignDialog(wallet); messageSignDialog = new MessageSignDialog(wallet);
} }

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent; import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi; import com.sparrowwallet.sparrow.io.Hwi;
@ -38,6 +39,7 @@ public class DevicePane extends TitledDescriptionPane {
private final Wallet wallet; private final Wallet wallet;
private final PSBT psbt; private final PSBT psbt;
private final KeyDerivation keyDerivation; private final KeyDerivation keyDerivation;
private final String message;
private final Device device; private final Device device;
private CustomPasswordField pinField; private CustomPasswordField pinField;
@ -47,6 +49,7 @@ public class DevicePane extends TitledDescriptionPane {
private SplitMenuButton importButton; private SplitMenuButton importButton;
private Button signButton; private Button signButton;
private Button displayAddressButton; private Button displayAddressButton;
private Button signMessageButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty(""); private final SimpleStringProperty passphrase = new SimpleStringProperty("");
@ -56,6 +59,7 @@ public class DevicePane extends TitledDescriptionPane {
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
this.keyDerivation = null; this.keyDerivation = null;
this.message = null;
this.device = device; this.device = device;
setDefaultStatus(); setDefaultStatus();
@ -81,6 +85,7 @@ public class DevicePane extends TitledDescriptionPane {
this.wallet = null; this.wallet = null;
this.psbt = psbt; this.psbt = psbt;
this.keyDerivation = null; this.keyDerivation = null;
this.message = null;
this.device = device; this.device = device;
setDefaultStatus(); setDefaultStatus();
@ -106,6 +111,7 @@ public class DevicePane extends TitledDescriptionPane {
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
this.keyDerivation = keyDerivation; this.keyDerivation = keyDerivation;
this.message = null;
this.device = device; this.device = device;
setDefaultStatus(); setDefaultStatus();
@ -125,6 +131,32 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton); 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 @Override
protected Control createButton() { protected Control createButton() {
createUnlockButton(); 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) { private void unlock(Device device) {
if(device.getModel().equals(WalletModel.TREZOR_1)) { if(device.getModel().equals(WalletModel.TREZOR_1)) {
promptPin(); promptPin();
@ -438,6 +486,20 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressService.start(); 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() { private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) { if(deviceOperation.equals(DeviceOperation.IMPORT)) {
importButton.setVisible(true); importButton.setVisible(true);
@ -450,6 +512,9 @@ public class DevicePane extends TitledDescriptionPane {
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) { } else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
displayAddressButton.setVisible(true); displayAddressButton.setVisible(true);
showHideLink.setVisible(false); 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 { public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS; IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
} }
} }

View file

@ -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<String> {
private final Wallet wallet;
private final String message;
private final KeyDerivation keyDerivation;
public DeviceSignMessageDialog(List<String> 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());
}
}
}

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -88,7 +89,8 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}); });
actionBox.getChildren().add(receiveButton); 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(""); Button signMessageButton = new Button("");
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY); Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
signMessageGlyph.setFontSize(12); signMessageGlyph.setFontSize(12);

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; 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.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.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.AppController;
@ -32,7 +34,7 @@ import tornadofx.control.Fieldset;
import tornadofx.control.Form; import tornadofx.control.Form;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.Arrays; import java.util.List;
import java.util.Optional; import java.util.Optional;
import static com.sparrowwallet.sparrow.AppController.setStageIcon; import static com.sparrowwallet.sparrow.AppController.setStageIcon;
@ -73,8 +75,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(wallet.getKeystores().size() != 1) { if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required"); throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
} }
if(!wallet.getKeystores().get(0).hasSeed()) { 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"); throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
} }
} }
@ -211,11 +213,15 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return; return;
} }
if(wallet.containsSeeds()) {
if(wallet.isEncrypted()) { if(wallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent()); EventManager.get().post(new RequestOpenWalletsEvent());
} else { } else {
signUnencryptedKeystore(wallet); signUnencryptedKeystore(wallet);
} }
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(wallet);
}
} }
private void signUnencryptedKeystore(Wallet decryptedWallet) { private void signUnencryptedKeystore(Wallet decryptedWallet) {
@ -232,6 +238,17 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} }
} }
private void signUsbKeystore(Wallet usbWallet) {
List<String> 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<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
signature.clear();
signature.appendText(optSignature.get());
}
}
private void verifyMessage() { private void verifyMessage() {
try { try {
//Find ECKey from message and signature //Find ECKey from message and signature

View file

@ -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;
}
}

View file

@ -115,14 +115,45 @@ public class Hwi {
JsonObject result = JsonParser.parseString(output).getAsJsonObject(); JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("address") != null) { if(result.get("address") != null) {
return result.get("address").getAsString(); return result.get("address").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new DisplayAddressException(error.getAsString());
} else { } else {
throw new DisplayAddressException("Could not retrieve address"); throw new DisplayAddressException("Could not retrieve address");
} }
}
} catch(IOException e) { } catch(IOException e) {
throw new DisplayAddressException(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 { public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
try { try {
String psbtBase64 = psbt.toBase64String(); String psbtBase64 = psbt.toBase64String();
@ -409,6 +440,30 @@ public class Hwi {
} }
} }
public static class SignMessageService extends Service<String> {
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<String> 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<String> { public static class GetXpubService extends Service<String> {
private final Device device; private final Device device;
private final String passphrase; private final String passphrase;
@ -479,6 +534,7 @@ public class Hwi {
PROMPT_PIN("promptpin", true), PROMPT_PIN("promptpin", true),
SEND_PIN("sendpin", false), SEND_PIN("sendpin", false),
DISPLAY_ADDRESS("displayaddress", true), DISPLAY_ADDRESS("displayaddress", true),
SIGN_MESSAGE("signmessage", true),
GET_XPUB("getxpub", true), GET_XPUB("getxpub", true),
SIGN_TX("signtx", true); SIGN_TX("signtx", true);

View file

@ -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);
}
}