mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
add airgapped message signing via qr
This commit is contained in:
parent
3dcbe34485
commit
201900aa0e
4 changed files with 104 additions and 18 deletions
|
@ -1292,8 +1292,7 @@ public class AppController implements Initializable {
|
||||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
if(selectedWalletForm != null) {
|
if(selectedWalletForm != null) {
|
||||||
Wallet wallet = selectedWalletForm.getWallet();
|
Wallet wallet = selectedWalletForm.getWallet();
|
||||||
if(wallet.getKeystores().size() == 1 &&
|
if(wallet.getKeystores().size() == 1) {
|
||||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
|
|
||||||
//Can sign and verify
|
//Can sign and verify
|
||||||
messageSignDialog = new MessageSignDialog(wallet);
|
messageSignDialog = new MessageSignDialog(wallet);
|
||||||
}
|
}
|
||||||
|
|
|
@ -399,9 +399,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
|
|
||||||
private static boolean canSignMessage(WalletNode walletNode) {
|
private static boolean canSignMessage(WalletNode walletNode) {
|
||||||
Wallet wallet = walletNode.getWallet();
|
Wallet wallet = walletNode.getWallet();
|
||||||
return wallet.getKeystores().size() == 1 &&
|
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB || wallet.getKeystores().get(0).getWalletModel().isCard()) &&
|
|
||||||
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||||
|
|
|
@ -12,10 +12,9 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
@ -23,6 +22,7 @@ import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.controlsfx.control.SegmentedButton;
|
import org.controlsfx.control.SegmentedButton;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.controlsfx.validation.ValidationResult;
|
import org.controlsfx.validation.ValidationResult;
|
||||||
import org.controlsfx.validation.ValidationSupport;
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
|
@ -38,6 +38,8 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||||
|
|
||||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
||||||
|
|
||||||
|
@ -50,6 +52,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
private final ToggleButton formatBip322;
|
private final ToggleButton formatBip322;
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
private WalletNode walletNode;
|
private WalletNode walletNode;
|
||||||
|
private boolean canSign;
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,10 +93,12 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
|
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
|
||||||
if(walletNode != null) {
|
if(walletNode != null) {
|
||||||
checkWalletSigning(walletNode.getWallet());
|
checkWalletSigning(walletNode.getWallet());
|
||||||
|
this.canSign = canSign(walletNode.getWallet());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
checkWalletSigning(wallet);
|
checkWalletSigning(wallet);
|
||||||
|
this.canSign = canSign(wallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
|
@ -172,6 +177,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
formatButtons.setDisable(true);
|
formatButtons.setDisable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ButtonType showQrButtonType = new javafx.scene.control.ButtonType("Sign by QR", ButtonBar.ButtonData.LEFT);
|
||||||
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
|
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 verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
|
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
@ -190,10 +196,20 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
|
||||||
|
|
||||||
|
Button showQrButton = (Button) dialogPane.lookupButton(showQrButtonType);
|
||||||
|
showQrButton.setDisable(wallet == null);
|
||||||
|
showQrButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||||
|
showQrButton.setGraphicTextGap(5);
|
||||||
|
showQrButton.setOnAction(event -> {
|
||||||
|
showQr();
|
||||||
|
});
|
||||||
|
|
||||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||||
signButton.setDisable(wallet == null);
|
signButton.setDisable(!canSign);
|
||||||
|
signButton.setGraphic(getGlyph(getSignGlyph()));
|
||||||
|
signButton.setGraphicTextGap(5);
|
||||||
signButton.setOnAction(event -> {
|
signButton.setOnAction(event -> {
|
||||||
signMessage();
|
signMessage();
|
||||||
});
|
});
|
||||||
|
@ -205,7 +221,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
});
|
});
|
||||||
|
|
||||||
boolean validAddress = isValidAddress();
|
boolean validAddress = isValidAddress();
|
||||||
signButton.setDisable(!validAddress || (wallet == null));
|
showQrButton.setDisable(!validAddress || (wallet == null));
|
||||||
|
signButton.setDisable(!validAddress || !canSign);
|
||||||
verifyButton.setDisable(!validAddress);
|
verifyButton.setDisable(!validAddress);
|
||||||
|
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
@ -216,7 +233,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
boolean valid = isValidAddress();
|
boolean valid = isValidAddress();
|
||||||
signButton.setDisable(!valid || (wallet == null));
|
showQrButton.setDisable(!valid || (wallet == null));
|
||||||
|
signButton.setDisable(!valid || !canSign);
|
||||||
verifyButton.setDisable(!valid);
|
verifyButton.setDisable(!valid);
|
||||||
|
|
||||||
if(valid) {
|
if(valid) {
|
||||||
|
@ -248,7 +266,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
setResultConverter(dialogButton -> dialogButton == showQrButtonType || dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if(address.getText().isEmpty()) {
|
if(address.getText().isEmpty()) {
|
||||||
|
@ -269,9 +287,12 @@ 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).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB && !wallet.getKeystores().get(0).getWalletModel().isCard()) {
|
}
|
||||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a connected keystore");
|
|
||||||
}
|
private boolean canSign(Wallet wallet) {
|
||||||
|
return wallet.getKeystores().get(0).hasPrivateKey()
|
||||||
|
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
||||||
|
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getAddress()throws InvalidAddressException {
|
private Address getAddress()throws InvalidAddressException {
|
||||||
|
@ -320,6 +341,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!canSign) {
|
||||||
|
AppServices.showErrorDialog("Wallet can't sign", "This wallet cannot sign a message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//Note we can expect a single keystore due to the check in the constructor
|
//Note we can expect a single keystore due to the check in the constructor
|
||||||
Wallet signingWallet = walletNode.getWallet();
|
Wallet signingWallet = walletNode.getWallet();
|
||||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||||
|
@ -430,6 +456,58 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
return providedAddress.equals(signedMessageAddress);
|
return providedAddress.equals(signedMessageAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showQr() {
|
||||||
|
if(walletNode == null) {
|
||||||
|
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Note we can expect a single keystore due to the check in the constructor
|
||||||
|
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||||
|
String derivationPath = KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), false);
|
||||||
|
|
||||||
|
String qrText = "signmessage " + derivationPath + " ascii:" + message.getText().trim();
|
||||||
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
||||||
|
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||||
|
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
||||||
|
scanQr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanQr() {
|
||||||
|
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||||
|
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||||
|
if(optionalResult.isPresent()) {
|
||||||
|
QRScanDialog.Result result = optionalResult.get();
|
||||||
|
if(result.payload != null) {
|
||||||
|
signature.clear();
|
||||||
|
signature.appendText(result.payload);
|
||||||
|
} else if(result.exception != null) {
|
||||||
|
log.error("Error scanning QR", result.exception);
|
||||||
|
showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||||
|
} else {
|
||||||
|
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a signature.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Glyph getSignGlyph() {
|
||||||
|
if(wallet != null) {
|
||||||
|
if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||||
|
return new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
|
||||||
|
} else if(wallet.getKeystores().get(0).getWalletModel().isCard()) {
|
||||||
|
return new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Glyph getGlyph(Glyph glyph) {
|
||||||
|
glyph.setFontSize(11);
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void openWallets(OpenWalletsEvent event) {
|
public void openWallets(OpenWalletsEvent event) {
|
||||||
Storage storage = event.getStorage(wallet);
|
Storage storage = event.getStorage(wallet);
|
||||||
|
|
|
@ -111,10 +111,15 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public QRDisplayDialog(String data) {
|
public QRDisplayDialog(String data) {
|
||||||
|
this(data, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QRDisplayDialog(String data, boolean addScanButton) {
|
||||||
this.ur = null;
|
this.ur = null;
|
||||||
this.encoder = null;
|
this.encoder = null;
|
||||||
|
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||||
|
setDialogPane(dialogPane);
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
|
||||||
StackPane stackPane = new StackPane();
|
StackPane stackPane = new StackPane();
|
||||||
|
@ -126,6 +131,12 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||||
|
|
||||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
|
||||||
|
if(addScanButton) {
|
||||||
|
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||||
|
dialogPane.getButtonTypes().add(scanButtonType);
|
||||||
|
}
|
||||||
|
|
||||||
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
|
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
|
||||||
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
|
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
Loading…
Reference in a new issue