From 7a99c4a11ad69758bf52c8e9a29c1630ef41ee65 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 27 Jan 2023 10:39:29 +0200 Subject: [PATCH] add tapsigner signing support and refactor card api --- drongo | 2 +- .../sparrow/control/CardImportPane.java | 45 ++---- .../sparrow/control/DeviceDialog.java | 6 +- .../sparrow/control/DevicePane.java | 104 +++++++++++--- .../sparrow/control/DeviceSignDialog.java | 40 +++++- .../com/sparrowwallet/sparrow/io/Device.java | 9 ++ .../sparrow/io/ckcard/CardApi.java | 129 ++++++++++++++++-- .../sparrow/io/ckcard/CardSign.java | 5 + .../sparrow/io/ckcard/CardTransport.java | 4 +- .../sparrow/io/ckcard/CkCard.java | 1 + .../transaction/HeadersController.java | 15 +- 11 files changed, 284 insertions(+), 76 deletions(-) diff --git a/drongo b/drongo index a14b23f2..e2a4c32d 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit a14b23f2fabc35c1c0b4b7b9f886dab10b4f7562 +Subproject commit e2a4c32db317b9e950cfbec822cc8103332d29ff diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java index 9f0f005f..e2c2adcd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java @@ -38,18 +38,16 @@ public class CardImportPane extends TitledDescriptionPane { private final List derivation; protected Button importButton; private final SimpleStringProperty pin = new SimpleStringProperty(""); - private final SimpleStringProperty errorText = new SimpleStringProperty(""); - private boolean initialized; public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) { - super(importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png"); + super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png"); this.importer = importer; this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation(); } @Override protected Control createButton() { - importButton = new Button("Tap"); + importButton = new Button("Import"); Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI); tapGlyph.setFontSize(12); importButton.setGraphic(tapGlyph); @@ -61,25 +59,23 @@ public class CardImportPane extends TitledDescriptionPane { } private void importCard() { - errorText.set(""); - try { if(!importer.isInitialized()) { setDescription("Card not initialized"); setContent(getInitializationPanel()); + showHideLink.setVisible(false); setExpanded(true); return; - } else { - initialized = true; } } catch(CardException e) { setError("Card Error", e.getMessage()); return; } - if(pin.get().isEmpty()) { - setDescription("Enter PIN code"); + if(pin.get().length() < 6) { + setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); setContent(getPinEntry()); + showHideLink.setVisible(false); setExpanded(true); return; } @@ -92,29 +88,18 @@ public class CardImportPane extends TitledDescriptionPane { EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue())); }); cardImportService.setOnFailed(event -> { - log.error("Error importing keystore from card", event.getSource().getException()); Throwable rootCause = Throwables.getRootCause(event.getSource().getException()); if(rootCause instanceof CardAuthorizationException) { - setError("Import Error", "Incorrect PIN code, try again:"); + setError(rootCause.getMessage(), null); + setContent(getPinEntry()); } else { + log.error("Error importing keystore from card", event.getSource().getException()); setError("Import Error", rootCause.getMessage()); } }); cardImportService.start(); } - @Override - protected void setError(String title, String detail) { - if(!initialized) { - super.setError(title, detail); - } else { - super.setError(title, null); - errorText.set(detail); - setContent(getPinEntry()); - setExpanded(true); - } - } - private Node getInitializationPanel() { VBox initTypeBox = new VBox(5); RadioButton automatic = new RadioButton("Automatic (Recommended)"); @@ -139,7 +124,6 @@ public class CardImportPane extends TitledDescriptionPane { byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8)); CardInitializationService cardInitializationService = new CardInitializationService(importer, chainCode); cardInitializationService.setOnSucceeded(event1 -> { - initialized = true; AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou will now need to enter the PIN code found on the back. You can change the PIN code once it has been imported."); setDescription("Enter PIN code"); setContent(getPinEntry()); @@ -164,17 +148,8 @@ public class CardImportPane extends TitledDescriptionPane { private Node getPinEntry() { VBox vBox = new VBox(); - if(!errorText.get().isEmpty()) { - Node errorBox = getContentBox(errorText.get()); - if(errorBox instanceof HBox hBox && hBox.getPrefHeight() == 60) { - hBox.setPrefHeight(50); - } - vBox.getChildren().add(errorBox); - } - CustomPasswordField pinField = new ViewPasswordField(); pinField.setPromptText("PIN Code"); - pinField.setText(pin.get()); importButton.setDefaultButton(true); pin.bind(pinField.textProperty()); HBox.setHgrow(pinField, Priority.ALWAYS); @@ -183,7 +158,7 @@ public class CardImportPane extends TitledDescriptionPane { contentBox.setAlignment(Pos.TOP_RIGHT); contentBox.setSpacing(20); contentBox.getChildren().add(pinField); - contentBox.setPadding(new Insets(errorText.get().isEmpty() ? 10 : 0, 30, 10, 30)); + contentBox.setPadding(new Insets(10, 30, 10, 30)); contentBox.setPrefHeight(50); vBox.getChildren().add(contentBox); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceDialog.java index 4b18119f..d201800b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DeviceDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceDialog.java @@ -68,7 +68,7 @@ public abstract class DeviceDialog extends Dialog { stackPane.getChildren().addAll(anchorPane, scanBox); - List devices = AppServices.getDevices(); + List devices = getDevices(); if(devices == null || devices.isEmpty()) { scanButton.setDefaultButton(true); scanBox.setVisible(true); @@ -96,6 +96,10 @@ public abstract class DeviceDialog extends Dialog { setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult()); } + protected List getDevices() { + return AppServices.getDevices(); + } + private void scan() { Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null); enumerateService.setOnSucceeded(workerStateEvent -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index affcae3c..6068fa35 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.control; +import com.google.common.base.Throwables; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.OutputDescriptor; @@ -8,18 +9,19 @@ import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.psbt.PSBT; -import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.KeystoreSource; -import com.sparrowwallet.drongo.wallet.StandardAccount; -import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Hwi; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.ckcard.CardApi; +import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException; import com.sparrowwallet.sparrow.net.ElectrumServer; import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.concurrent.Service; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -60,6 +62,8 @@ public class DevicePane extends TitledDescriptionPane { private Button discoverKeystoresButton; private final SimpleStringProperty passphrase = new SimpleStringProperty(""); + private final SimpleStringProperty pin = new SimpleStringProperty(""); + private final StringProperty messageProperty = new SimpleStringProperty(""); private boolean defaultDevice; @@ -86,10 +90,10 @@ public class DevicePane extends TitledDescriptionPane { buttonBox.getChildren().addAll(setPassphraseButton, importButton); } - public DevicePane(PSBT psbt, Device device, boolean defaultDevice) { + public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) { super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); this.deviceOperation = DeviceOperation.SIGN; - this.wallet = null; + this.wallet = wallet; this.psbt = psbt; this.outputDescriptor = null; this.keyDerivation = null; @@ -106,6 +110,10 @@ public class DevicePane extends TitledDescriptionPane { initialise(device); + messageProperty.addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> setDescription(newValue)); + }); + buttonBox.getChildren().addAll(setPassphraseButton, signButton); } @@ -201,7 +209,7 @@ public class DevicePane extends TitledDescriptionPane { } private void setDefaultStatus() { - setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked"); + setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Place card on reader" : "Unlocked"); } private void createUnlockButton() { @@ -617,19 +625,54 @@ public class DevicePane extends TitledDescriptionPane { } private void sign() { - Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt); - signPSBTService.setOnSucceeded(workerStateEvent -> { - PSBT signedPsbt = signPSBTService.getValue(); - EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt)); - }); - signPSBTService.setOnFailed(workerStateEvent -> { - setError("Signing Error", signPSBTService.getException().getMessage()); - log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException()); - signButton.setDisable(false); - }); - setDescription("Signing..."); - showHideLink.setVisible(false); - signPSBTService.start(); + if(device.isCard()) { + if(pin.get().length() < 6) { + setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); + setContent(getCardPinEntry()); + setExpanded(true); + signButton.setDisable(false); + return; + } + + try { + CardApi cardApi = new CardApi(pin.get()); + + Service signService = cardApi.getSignService(wallet, psbt, messageProperty); + signService.setOnSucceeded(event -> { + EventManager.get().post(new PSBTSignedEvent(psbt, psbt)); + }); + signService.setOnFailed(event -> { + Throwable rootCause = Throwables.getRootCause(event.getSource().getException()); + if(rootCause instanceof CardAuthorizationException) { + setError(rootCause.getMessage(), null); + setContent(getCardPinEntry()); + } else { + log.error("Signing Error: " + rootCause.getMessage(), event.getSource().getException()); + setError("Signing Error", rootCause.getMessage()); + } + signButton.setDisable(false); + }); + signService.start(); + } catch(Exception e) { + log.error("Signing Error: " + e.getMessage(), e); + setError("Signing Error", e.getMessage()); + signButton.setDisable(false); + } + } else { + Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt); + signPSBTService.setOnSucceeded(workerStateEvent -> { + PSBT signedPsbt = signPSBTService.getValue(); + EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt)); + }); + signPSBTService.setOnFailed(workerStateEvent -> { + setError("Signing Error", signPSBTService.getException().getMessage()); + log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException()); + signButton.setDisable(false); + }); + setDescription("Signing..."); + showHideLink.setVisible(false); + signPSBTService.start(); + } } private void displayAddress() { @@ -785,6 +828,27 @@ public class DevicePane extends TitledDescriptionPane { return contentBox; } + private Node getCardPinEntry() { + VBox vBox = new VBox(); + + CustomPasswordField pinField = new ViewPasswordField(); + pinField.setPromptText("PIN Code"); + signButton.setDefaultButton(true); + pin.bind(pinField.textProperty()); + HBox.setHgrow(pinField, Priority.ALWAYS); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.TOP_RIGHT); + contentBox.setSpacing(20); + contentBox.getChildren().add(pinField); + contentBox.setPadding(new Insets(10, 30, 10, 30)); + contentBox.setPrefHeight(50); + + vBox.getChildren().add(contentBox); + + return vBox; + } + public Device getDevice() { return device; } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java index dbadb5bf..c53758fa 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java @@ -2,17 +2,28 @@ package com.sparrowwallet.sparrow.control; import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.psbt.PSBT; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.io.Device; +import com.sparrowwallet.sparrow.io.ckcard.CardApi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.smartcardio.CardException; +import java.util.ArrayList; import java.util.List; public class DeviceSignDialog extends DeviceDialog { + private static final Logger log = LoggerFactory.getLogger(DeviceSignDialog.class); + + private final Wallet wallet; private final PSBT psbt; - public DeviceSignDialog(List operationFingerprints, PSBT psbt) { + public DeviceSignDialog(Wallet wallet, List operationFingerprints, PSBT psbt) { super(operationFingerprints); + this.wallet = wallet; this.psbt = psbt; EventManager.get().register(this); setOnCloseRequest(event -> { @@ -21,9 +32,34 @@ public class DeviceSignDialog extends DeviceDialog { setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt); } + @Override + protected List getDevices() { + List devices = super.getDevices(); + + if(CardApi.isReaderAvailable()) { + devices = new ArrayList<>(devices); + try { + CardApi cardApi = new CardApi(null); + if(cardApi.isInitialized()) { + Device cardDevice = new Device(); + cardDevice.setType(WalletModel.TAPSIGNER.getType()); + cardDevice.setModel(WalletModel.TAPSIGNER); + cardDevice.setNeedsPassphraseSent(Boolean.FALSE); + cardDevice.setNeedsPinSent(Boolean.FALSE); + cardDevice.setCard(true); + devices.add(cardDevice); + } + } catch(CardException e) { + log.error("Error reading card", e); + } + } + + return devices; + } + @Override protected DevicePane getDevicePane(Device device, boolean defaultDevice) { - return new DevicePane(psbt, device, defaultDevice); + return new DevicePane(wallet, psbt, device, defaultDevice); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Device.java b/src/main/java/com/sparrowwallet/sparrow/io/Device.java index 3cb35bae..bc066103 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Device.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Device.java @@ -11,6 +11,7 @@ public class Device { private Boolean needsPinSent; private Boolean needsPassphraseSent; private String fingerprint; + private boolean card; private String[][] warnings; private String error; @@ -70,6 +71,14 @@ public class Device { this.fingerprint = fingerprint; } + public boolean isCard() { + return card; + } + + public void setCard(boolean card) { + this.card = card; + } + public boolean containsWarning(String warning) { if(warnings != null) { for(String[] warns : warnings) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java index c93adb55..aa02ab1f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java @@ -4,10 +4,15 @@ import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.Base58; -import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.KeystoreSource; -import com.sparrowwallet.drongo.wallet.WalletModel; +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.protocol.SigHash; +import com.sparrowwallet.drongo.protocol.TransactionSignature; +import com.sparrowwallet.drongo.psbt.PSBT; +import com.sparrowwallet.drongo.psbt.PSBTInput; +import com.sparrowwallet.drongo.psbt.PSBTInputSigner; +import com.sparrowwallet.drongo.wallet.*; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; @@ -19,6 +24,8 @@ import org.slf4j.LoggerFactory; import javax.smartcardio.CardException; import java.util.List; +import java.util.Map; +import java.util.Optional; public class CardApi { private static final Logger log = LoggerFactory.getLogger(CardApi.class); @@ -106,13 +113,7 @@ public class CardApi { } public Keystore getKeystore() throws CardException { - CardStatus cardStatus = getStatus(); - - CardXpub masterXpub = cardProtocol.xpub(cvc, true); - ExtendedKey masterXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(masterXpub.xpub)); - String masterFingerprint = Utils.bytesToHex(masterXpubkey.getKey().getFingerprint()); - - KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, cardStatus.getDerivation()); + KeyDerivation keyDerivation = getKeyDerivation(); CardXpub derivedXpub = cardProtocol.xpub(cvc, false); ExtendedKey derivedXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(derivedXpub.xpub)); @@ -127,6 +128,59 @@ public class CardApi { return keystore; } + private KeyDerivation getKeyDerivation() throws CardException { + String masterFingerprint = getMasterFingerprint(); + return new KeyDerivation(masterFingerprint, getStatus().getDerivation()); + } + + private String getMasterFingerprint() throws CardException { + CardXpub masterXpub = cardProtocol.xpub(cvc, true); + ExtendedKey masterXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(masterXpub.xpub)); + return Utils.bytesToHex(masterXpubkey.getKey().getFingerprint()); + } + + public Service getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) { + return new SignService(wallet, psbt, messageProperty); + } + + void sign(Wallet wallet, PSBT psbt) throws CardException { + Keystore cardKeystore = getKeystore(); + KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation(); + + Map signingNodes = wallet.getSigningNodes(psbt); + for(PSBTInput psbtInput : psbt.getPsbtInputs()) { + if(!psbtInput.isSigned()) { + WalletNode signingNode = signingNodes.get(psbtInput); + KeyDerivation changedDerivation = null; + try { + ECKey cardSigningPubKey = cardKeystore.getPubKey(signingNode); + if(wallet.getKeystores().stream().noneMatch(keystore -> keystore.getPubKey(signingNode).equals(cardSigningPubKey))) { + Optional optKeyDerivation = wallet.getKeystores().stream().map(Keystore::getKeyDerivation) + .filter(kd -> kd.getMasterFingerprint().equals(cardKeyDerivation.getMasterFingerprint()) && !kd.getDerivation().equals(cardKeyDerivation.getDerivation())).findFirst(); + if(optKeyDerivation.isPresent()) { + changedDerivation = optKeyDerivation.get(); + setDerivation(changedDerivation.getDerivation()); + + Keystore changedKeystore = getKeystore(); + ECKey changedSigningPubKey = changedKeystore.getPubKey(signingNode); + if(wallet.getKeystores().stream().noneMatch(keystore -> keystore.getPubKey(signingNode).equals(changedSigningPubKey))) { + throw new CardException("Card cannot recognise public key for signing address."); + } + } else { + throw new CardException("Card cannot recognise public key for signing address."); + } + } + + psbtInput.sign(new CardPSBTInputSigner(signingNode)); + } finally { + if(changedDerivation != null) { + setDerivation(cardKeyDerivation.getDerivation()); + } + } + } + } + } + public void disconnect() { try { cardProtocol.disconnect(); @@ -175,4 +229,59 @@ public class CardApi { }; } } + + public class SignService extends Service { + private final Wallet wallet; + private final PSBT psbt; + private final StringProperty messageProperty; + + public SignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) { + this.wallet = wallet; + this.psbt = psbt; + this.messageProperty = messageProperty; + } + + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Void call() throws Exception { + CardStatus cardStatus = getStatus(); + checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); + + sign(wallet, psbt); + return null; + } + }; + } + } + + private class CardPSBTInputSigner implements PSBTInputSigner { + private final WalletNode signingNode; + private ECKey pubkey; + + public CardPSBTInputSigner(WalletNode signingNode) { + this.signingNode = signingNode; + } + + @Override + public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) { + if(signatureType != TransactionSignature.Type.ECDSA) { + throw new IllegalStateException(cardType.toDisplayString() + " cannot sign " + signatureType + " transactions."); + } + + try { + CardSign cardSign = cardProtocol.sign(cvc, signingNode.getDerivation(), hash); + pubkey = cardSign.getPubKey(); + return new TransactionSignature(cardSign.getSignature(), sigHash); + } catch(CardException e) { + throw new RuntimeException(e); + } + } + + @Override + public ECKey getPubKey() { + return pubkey; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java index 42afd4aa..67ec027b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.io.ckcard; import com.sparrowwallet.drongo.crypto.ECDSASignature; +import com.sparrowwallet.drongo.crypto.ECKey; import java.math.BigInteger; import java.util.Arrays; @@ -19,4 +20,8 @@ public class CardSign extends CardResponse { return null; } + + public ECKey getPubKey() { + return ECKey.fromPublicOnly(pubkey); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java index b2de7f83..4bce8a7d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java @@ -108,9 +108,9 @@ public class CardTransport { String msg = result.get("error").getAsString(); int code = result.get("code") == null ? 500 : result.get("code").getAsInt(); if(code == 205) { - throw new CardUnluckyNumberException("Card chose unlucky number, please retry."); + throw new CardUnluckyNumberException("Card chose unlucky number, please retry"); } else if(code == 401) { - throw new CardAuthorizationException("Incorrect PIN provided."); + throw new CardAuthorizationException("Incorrect PIN provided"); } throw new CardException(code + " on " + cmd + ": " + msg); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java index 478b89ad..c931de11 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java @@ -88,6 +88,7 @@ public class CkCard implements KeystoreCardImport { return WalletModel.TAPSIGNER; } + @Override public StringProperty messageProperty() { return messageProperty; } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index b8acd760..d5ef19f3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -754,8 +754,13 @@ public class HeadersController extends TransactionFormController implements Init Optional softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny(); Optional usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)).findAny(); Optional bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny(); - if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty()) { + Optional cardKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getWalletModel().equals(WalletModel.TAPSIGNER)).findAny(); + if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty() && cardKeystore.isEmpty()) { signButton.setDisable(true); + } else if(softwareKeystore.isEmpty() && bip47Keystore.isEmpty() && usbKeystore.isEmpty()) { + Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI); + tapGlyph.setFontSize(20); + signButton.setGraphic(tapGlyph); } else if(softwareKeystore.isEmpty() && bip47Keystore.isEmpty()) { Glyph usbGlyph = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB); usbGlyph.setFontSize(20); @@ -935,7 +940,7 @@ public class HeadersController extends TransactionFormController implements Init public void signPSBT(ActionEvent event) { signSoftwareKeystores(); - signUsbKeystores(); + signDeviceKeystores(); } private void signSoftwareKeystores() { @@ -982,7 +987,7 @@ public class HeadersController extends TransactionFormController implements Init } } - private void signUsbKeystores() { + private void signDeviceKeystores() { if(headersForm.getPsbt().isSigned()) { return; } @@ -990,12 +995,12 @@ public class HeadersController extends TransactionFormController implements Init List fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList()); List signingDevices = AppServices.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList()); if(signingDevices.isEmpty() && - (headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)) || + (headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH) || keystore.getWalletModel().equals(WalletModel.TAPSIGNER)) || (headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)) && headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.SW_WATCH))))) { return; } - DeviceSignDialog dlg = new DeviceSignDialog(fingerprints, headersForm.getPsbt()); + DeviceSignDialog dlg = new DeviceSignDialog(headersForm.getSigningWallet(), fingerprints, headersForm.getPsbt()); dlg.initModality(Modality.NONE); Stage stage = (Stage)dlg.getDialogPane().getScene().getWindow(); stage.setAlwaysOnTop(true);