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

View file

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

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.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<Entry, Entry> {
});
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);

View file

@ -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<ButtonBar.ButtonData> {
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<ButtonBar.ButtonData> {
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<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() {
try {
//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

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

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