mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add message signing/verifying functionality
This commit is contained in:
parent
9dd52f120f
commit
6c90a7e0ba
6 changed files with 340 additions and 5 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit 6b4b2529c803581b13e1604acd230dff24c839b4
|
||||
Subproject commit ee49ddd94bdfec1e87d334f17833a63382958790
|
|
@ -848,6 +848,26 @@ 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()) {
|
||||
//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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -71,12 +71,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(), nodeEntry));
|
||||
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 +86,21 @@ 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()) {
|
||||
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);
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
setText(hashIndexEntry.getDescription());
|
||||
|
@ -178,7 +193,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 +219,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,286 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
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.wallet.Keystore;
|
||||
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.Arrays;
|
||||
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()) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed");
|
||||
}
|
||||
}
|
||||
|
||||
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.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
signUnencryptedKeystore(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(), 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 verifyMessage() {
|
||||
try {
|
||||
//Find ECKey from message and signature
|
||||
//http://www.secg.org/download/aid-780/sec1-v2.pdf section 4.1.6
|
||||
|
||||
ECKey signedMessageKey = ECKey.signedMessageToKey(message.getText(), signature.getText());
|
||||
Address address = getAddress();
|
||||
|
||||
byte[] pubKeyHash = address.getOutputScriptData();
|
||||
byte[] signedKeyHash = signedMessageKey.getPubKeyHash();
|
||||
|
||||
if(!Arrays.equals(pubKeyHash, signedKeyHash)) {
|
||||
throw new SignatureException("Address pubkey hash did not match signed pubkey hash");
|
||||
}
|
||||
|
||||
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();
|
||||
} catch(SignatureException e) {
|
||||
AppController.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
|
||||
} catch(Exception e) {
|
||||
log.error("Could not verify message", e);
|
||||
AppController.showErrorDialog("Could not verify message", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue