Compare commits

...
6 Commits
16 changed files with 602 additions and 12 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.17.4'
}
def sparrowVersion = '0.9.2'
def sparrowVersion = '0.9.3'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
+1 -1
Submodule drongo updated: c7e16a29e3...10ebfe463d
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.9.2</string>
<string>0.9.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -848,6 +848,27 @@ public class AppController implements Initializable {
preferencesDialog.showAndWait();
}
public void signVerifyMessage(ActionEvent event) {
MessageSignDialog messageSignDialog = null;
Tab tab = tabs.getSelectionModel().getSelectedItem();
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() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
//Can sign and verify
messageSignDialog = new MessageSignDialog(wallet);
}
}
if(messageSignDialog == null) {
//Can verify only
messageSignDialog = new MessageSignDialog();
}
messageSignDialog.showAndWait();
}
public Tab addWalletTab(Storage storage, Wallet wallet) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "0.9.2";
public static final String APP_VERSION = "0.9.3";
private Stage mainStage;
@@ -32,7 +32,7 @@ public class AddressCell extends TreeTableCell<Entry, Entry> {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor()));
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), null));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
setTooltip(tooltip);
@@ -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;
}
}
@@ -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());
}
}
}
@@ -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;
@@ -71,12 +72,13 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor()));
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), null));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath());
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
@@ -85,7 +87,23 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
setGraphic(receiveButton);
actionBox.getChildren().add(receiveButton);
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);
signMessageButton.setGraphic(signMessageGlyph);
signMessageButton.setOnAction(event -> {
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
actionBox.getChildren().add(signMessageButton);
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
}
setGraphic(actionBox);
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
@@ -178,7 +196,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor) {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
@@ -204,6 +222,17 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
});
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
if(nodeEntry != null) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setOnAction(AE -> {
hide();
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
getItems().add(signVerifyMessage);
}
}
}
@@ -0,0 +1,329 @@
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;
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;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.security.SignatureException;
import java.util.List;
import java.util.Optional;
import static com.sparrowwallet.sparrow.AppController.setStageIcon;
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
private final TextField address;
private final TextArea message;
private final TextArea signature;
private final Wallet wallet;
private WalletNode walletNode;
/**
* Verification only constructor
*/
public MessageSignDialog() {
this(null, null);
}
/**
* Sign and verify with user entered address
*
* @param wallet Wallet to sign with
*/
public MessageSignDialog(Wallet wallet) {
this(wallet, null);
}
/**
* Sign and verify with preset address
*
* @param wallet Wallet to sign with
* @param walletNode Wallet node to derive address from
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
if(wallet != null) {
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() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
}
}
this.wallet = wallet;
this.walletNode = walletNode;
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(wallet == null ? "Verify Message" : "Sign/Verify Message");
Image image = new Image("image/seed.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
VBox vBox = new VBox();
vBox.setSpacing(20);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
Field addressField = new Field();
addressField.setText("Address:");
address = new TextField();
address.getStyleClass().add("id");
address.setEditable(walletNode == null);
addressField.getInputs().add(address);
if(walletNode != null) {
address.setText(wallet.getAddress(walletNode).toString());
}
Field messageField = new Field();
messageField.setText("Message:");
message = new TextArea();
message.setWrapText(true);
message.setPrefRowCount(10);
message.setStyle("-fx-pref-height: 180px");
messageField.getInputs().add(message);
Field signatureField = new Field();
signatureField.setText("Signature:");
signature = new TextArea();
signature.getStyleClass().add("id");
signature.setPrefRowCount(2);
signature.setStyle("-fx-pref-height: 60px");
signature.setWrapText(true);
signatureField.getInputs().add(signature);
fieldset.getChildren().addAll(addressField, messageField, signatureField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
Button signButton = (Button)dialogPane.lookupButton(signButtonType);
signButton.setDisable(wallet == null);
signButton.setOnAction(event -> {
signMessage();
});
Button verifyButton = (Button)dialogPane.lookupButton(verifyButtonType);
verifyButton.setDefaultButton(false);
verifyButton.setOnAction(event -> {
verifyMessage();
});
boolean validAddress = isValidAddress();
signButton.setDisable(!validAddress || (wallet == null));
verifyButton.setDisable(!validAddress);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
boolean valid = isValidAddress();
signButton.setDisable(!valid || (wallet == null));
verifyButton.setDisable(!valid);
if(valid && wallet != null) {
try {
Address address = getAddress();
setWalletNodeFromAddress(wallet, address);
} catch(InvalidAddressException e) {
//can't happen
}
}
});
EventManager.get().register(this);
setOnCloseRequest(event -> {
if(ButtonBar.ButtonData.APPLY.equals(getResult())) {
event.consume();
return;
}
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : ButtonBar.ButtonData.OK_DONE);
}
private Address getAddress()throws InvalidAddressException {
return Address.fromString(address.getText());
}
private boolean isValidAddress() {
try {
getAddress();
return true;
} catch (InvalidAddressException e) {
return false;
}
}
private void setWalletNodeFromAddress(Wallet wallet, Address address) {
walletNode = wallet.getWalletAddresses().get(address);
}
private void signMessage() {
if(walletNode == null) {
AppController.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
if(wallet.containsSeeds()) {
if(wallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
signUnencryptedKeystore(wallet);
}
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(wallet);
}
}
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
//Note we can expect a single keystore due to the check above
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
signature.clear();
signature.appendText(signatureText);
} catch(Exception e) {
log.error("Could not sign message", e);
AppController.showErrorDialog("Could not sign message", e.getMessage());
}
}
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
//http://www.secg.org/download/aid-780/sec1-v2.pdf section 4.1.6
boolean verified = false;
try {
ECKey signedMessageKey = ECKey.signedMessageToKey(message.getText().trim(), signature.getText().trim(), false);
verified = verifyMessage(signedMessageKey);
} catch(SignatureException e) {
//ignore
}
if(!verified) {
try {
ECKey electrumSignedMessageKey = ECKey.signedMessageToKey(message.getText(), signature.getText(), true);
verified = verifyMessage(electrumSignedMessageKey);
} catch(SignatureException e) {
//ignore
}
}
if(verified) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.setTitle("Verification Succeeded");
alert.setHeaderText("Verification Succeeded");
alert.setContentText("The signature verified against the message.");
alert.showAndWait();
} else {
AppController.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
}
} catch(IllegalArgumentException e) {
AppController.showErrorDialog("Could not verify message", e.getMessage());
} catch(Exception e) {
log.error("Could not verify message", e);
AppController.showErrorDialog("Could not verify message", e.getMessage());
}
}
private boolean verifyMessage(ECKey signedMessageKey) throws InvalidAddressException, SignatureException {
Address providedAddress = getAddress();
ScriptType scriptType = providedAddress.getScriptType();
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
Storage storage = event.getStorage(wallet);
if(storage == null) {
throw new IllegalStateException("Wallet " + wallet + " without Storage");
}
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
signUnencryptedKeystore(decryptedWallet);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
AppController.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
}
}
@@ -188,7 +188,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
confirmButton = new Button("Confirm Written Backup");
confirmButton = new Button("Re-enter Words...");
confirmButton.setOnAction(event -> {
confirmBackup();
});
@@ -269,7 +269,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
private void displayMnemonicCode() {
setDescription("Write down word list to confirm backup");
setDescription("Write down words before re-entering");
showHideLink.setVisible(false);
calculateButton.setVisible(false);
@@ -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;
}
}
@@ -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);
@@ -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);
}
}
@@ -81,6 +81,10 @@ public class UR {
}
}
public String toString() {
return UREncoder.encode(this);
}
@Override
public boolean equals(Object o) {
if(this == o) {
@@ -63,6 +63,9 @@
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
</items>
</Menu>
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" onAction="#signVerifyMessage"/>
</Menu>
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
<MenuItem styleClass="macHide" mnemonicParsing="false" text="About Sparrow" onAction="#showAbout"/>
</Menu>