add airgapped message signing via qr

This commit is contained in:
Craig Raw 2023-09-25 13:39:51 +02:00
parent 3dcbe34485
commit 201900aa0e
4 changed files with 104 additions and 18 deletions

View file

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

View file

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

View file

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

View file

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