add tapsigner signing support and refactor card api

This commit is contained in:
Craig Raw 2023-01-27 10:39:29 +02:00
parent 6c13504644
commit 7a99c4a11a
11 changed files with 284 additions and 76 deletions

2
drongo

@ -1 +1 @@
Subproject commit a14b23f2fabc35c1c0b4b7b9f886dab10b4f7562 Subproject commit e2a4c32db317b9e950cfbec822cc8103332d29ff

View file

@ -38,18 +38,16 @@ public class CardImportPane extends TitledDescriptionPane {
private final List<ChildNumber> derivation; private final List<ChildNumber> derivation;
protected Button importButton; protected Button importButton;
private final SimpleStringProperty pin = new SimpleStringProperty(""); private final SimpleStringProperty pin = new SimpleStringProperty("");
private final SimpleStringProperty errorText = new SimpleStringProperty("");
private boolean initialized;
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) { 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.importer = importer;
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation(); this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
} }
@Override @Override
protected Control createButton() { protected Control createButton() {
importButton = new Button("Tap"); importButton = new Button("Import");
Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI); Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
tapGlyph.setFontSize(12); tapGlyph.setFontSize(12);
importButton.setGraphic(tapGlyph); importButton.setGraphic(tapGlyph);
@ -61,25 +59,23 @@ public class CardImportPane extends TitledDescriptionPane {
} }
private void importCard() { private void importCard() {
errorText.set("");
try { try {
if(!importer.isInitialized()) { if(!importer.isInitialized()) {
setDescription("Card not initialized"); setDescription("Card not initialized");
setContent(getInitializationPanel()); setContent(getInitializationPanel());
showHideLink.setVisible(false);
setExpanded(true); setExpanded(true);
return; return;
} else {
initialized = true;
} }
} catch(CardException e) { } catch(CardException e) {
setError("Card Error", e.getMessage()); setError("Card Error", e.getMessage());
return; return;
} }
if(pin.get().isEmpty()) { if(pin.get().length() < 6) {
setDescription("Enter PIN code"); setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getPinEntry()); setContent(getPinEntry());
showHideLink.setVisible(false);
setExpanded(true); setExpanded(true);
return; return;
} }
@ -92,29 +88,18 @@ public class CardImportPane extends TitledDescriptionPane {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue())); EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
}); });
cardImportService.setOnFailed(event -> { cardImportService.setOnFailed(event -> {
log.error("Error importing keystore from card", event.getSource().getException());
Throwable rootCause = Throwables.getRootCause(event.getSource().getException()); Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
if(rootCause instanceof CardAuthorizationException) { if(rootCause instanceof CardAuthorizationException) {
setError("Import Error", "Incorrect PIN code, try again:"); setError(rootCause.getMessage(), null);
setContent(getPinEntry());
} else { } else {
log.error("Error importing keystore from card", event.getSource().getException());
setError("Import Error", rootCause.getMessage()); setError("Import Error", rootCause.getMessage());
} }
}); });
cardImportService.start(); 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() { private Node getInitializationPanel() {
VBox initTypeBox = new VBox(5); VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)"); 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)); byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
CardInitializationService cardInitializationService = new CardInitializationService(importer, chainCode); CardInitializationService cardInitializationService = new CardInitializationService(importer, chainCode);
cardInitializationService.setOnSucceeded(event1 -> { 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."); 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"); setDescription("Enter PIN code");
setContent(getPinEntry()); setContent(getPinEntry());
@ -164,17 +148,8 @@ public class CardImportPane extends TitledDescriptionPane {
private Node getPinEntry() { private Node getPinEntry() {
VBox vBox = new VBox(); 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(); CustomPasswordField pinField = new ViewPasswordField();
pinField.setPromptText("PIN Code"); pinField.setPromptText("PIN Code");
pinField.setText(pin.get());
importButton.setDefaultButton(true); importButton.setDefaultButton(true);
pin.bind(pinField.textProperty()); pin.bind(pinField.textProperty());
HBox.setHgrow(pinField, Priority.ALWAYS); HBox.setHgrow(pinField, Priority.ALWAYS);
@ -183,7 +158,7 @@ public class CardImportPane extends TitledDescriptionPane {
contentBox.setAlignment(Pos.TOP_RIGHT); contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20); contentBox.setSpacing(20);
contentBox.getChildren().add(pinField); 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); contentBox.setPrefHeight(50);
vBox.getChildren().add(contentBox); vBox.getChildren().add(contentBox);

View file

@ -68,7 +68,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
stackPane.getChildren().addAll(anchorPane, scanBox); stackPane.getChildren().addAll(anchorPane, scanBox);
List<Device> devices = AppServices.getDevices(); List<Device> devices = getDevices();
if(devices == null || devices.isEmpty()) { if(devices == null || devices.isEmpty()) {
scanButton.setDefaultButton(true); scanButton.setDefaultButton(true);
scanBox.setVisible(true); scanBox.setVisible(true);
@ -96,6 +96,10 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult()); setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
} }
protected List<Device> getDevices() {
return AppServices.getDevices();
}
private void scan() { private void scan() {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null); Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> { enumerateService.setOnSucceeded(workerStateEvent -> {

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor; 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.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi; import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; 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 com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
@ -60,6 +62,8 @@ public class DevicePane extends TitledDescriptionPane {
private Button discoverKeystoresButton; private Button discoverKeystoresButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty(""); private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private final SimpleStringProperty pin = new SimpleStringProperty("");
private final StringProperty messageProperty = new SimpleStringProperty("");
private boolean defaultDevice; private boolean defaultDevice;
@ -86,10 +90,10 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, importButton); 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"); super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN; this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null; this.wallet = wallet;
this.psbt = psbt; this.psbt = psbt;
this.outputDescriptor = null; this.outputDescriptor = null;
this.keyDerivation = null; this.keyDerivation = null;
@ -106,6 +110,10 @@ public class DevicePane extends TitledDescriptionPane {
initialise(device); initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().addAll(setPassphraseButton, signButton); buttonBox.getChildren().addAll(setPassphraseButton, signButton);
} }
@ -201,7 +209,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
private void setDefaultStatus() { 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() { private void createUnlockButton() {
@ -617,6 +625,40 @@ public class DevicePane extends TitledDescriptionPane {
} }
private void sign() { private void sign() {
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<Void> 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); Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
signPSBTService.setOnSucceeded(workerStateEvent -> { signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue(); PSBT signedPsbt = signPSBTService.getValue();
@ -631,6 +673,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(false); showHideLink.setVisible(false);
signPSBTService.start(); signPSBTService.start();
} }
}
private void displayAddress() { private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor); Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
@ -785,6 +828,27 @@ public class DevicePane extends TitledDescriptionPane {
return contentBox; 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() { public Device getDevice() {
return device; return device;
} }

View file

@ -2,17 +2,28 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.psbt.PSBT; 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.EventManager;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent; import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.io.Device; 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; import java.util.List;
public class DeviceSignDialog extends DeviceDialog<PSBT> { public class DeviceSignDialog extends DeviceDialog<PSBT> {
private static final Logger log = LoggerFactory.getLogger(DeviceSignDialog.class);
private final Wallet wallet;
private final PSBT psbt; private final PSBT psbt;
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) { public DeviceSignDialog(Wallet wallet, List<String> operationFingerprints, PSBT psbt) {
super(operationFingerprints); super(operationFingerprints);
this.wallet = wallet;
this.psbt = psbt; this.psbt = psbt;
EventManager.get().register(this); EventManager.get().register(this);
setOnCloseRequest(event -> { setOnCloseRequest(event -> {
@ -21,9 +32,34 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt); setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt);
} }
@Override
protected List<Device> getDevices() {
List<Device> 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 @Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) { protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(psbt, device, defaultDevice); return new DevicePane(wallet, psbt, device, defaultDevice);
} }
@Subscribe @Subscribe

View file

@ -11,6 +11,7 @@ public class Device {
private Boolean needsPinSent; private Boolean needsPinSent;
private Boolean needsPassphraseSent; private Boolean needsPassphraseSent;
private String fingerprint; private String fingerprint;
private boolean card;
private String[][] warnings; private String[][] warnings;
private String error; private String error;
@ -70,6 +71,14 @@ public class Device {
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
} }
public boolean isCard() {
return card;
}
public void setCard(boolean card) {
this.card = card;
}
public boolean containsWarning(String warning) { public boolean containsWarning(String warning) {
if(warnings != null) { if(warnings != null) {
for(String[] warns : warnings) { for(String[] warns : warnings) {

View file

@ -4,10 +4,15 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Base58; import com.sparrowwallet.drongo.protocol.Base58;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.protocol.SigHash;
import com.sparrowwallet.drongo.wallet.WalletModel; 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.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
@ -19,6 +24,8 @@ import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException; import javax.smartcardio.CardException;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
public class CardApi { public class CardApi {
private static final Logger log = LoggerFactory.getLogger(CardApi.class); private static final Logger log = LoggerFactory.getLogger(CardApi.class);
@ -106,13 +113,7 @@ public class CardApi {
} }
public Keystore getKeystore() throws CardException { public Keystore getKeystore() throws CardException {
CardStatus cardStatus = getStatus(); KeyDerivation keyDerivation = getKeyDerivation();
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());
CardXpub derivedXpub = cardProtocol.xpub(cvc, false); CardXpub derivedXpub = cardProtocol.xpub(cvc, false);
ExtendedKey derivedXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(derivedXpub.xpub)); ExtendedKey derivedXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(derivedXpub.xpub));
@ -127,6 +128,59 @@ public class CardApi {
return keystore; 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<Void> 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<PSBTInput, WalletNode> 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<KeyDerivation> 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() { public void disconnect() {
try { try {
cardProtocol.disconnect(); cardProtocol.disconnect();
@ -175,4 +229,59 @@ public class CardApi {
}; };
} }
} }
public class SignService extends Service<Void> {
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<Void> 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;
}
}
} }

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io.ckcard; package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.crypto.ECDSASignature; import com.sparrowwallet.drongo.crypto.ECDSASignature;
import com.sparrowwallet.drongo.crypto.ECKey;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
@ -19,4 +20,8 @@ public class CardSign extends CardResponse {
return null; return null;
} }
public ECKey getPubKey() {
return ECKey.fromPublicOnly(pubkey);
}
} }

View file

@ -108,9 +108,9 @@ public class CardTransport {
String msg = result.get("error").getAsString(); String msg = result.get("error").getAsString();
int code = result.get("code") == null ? 500 : result.get("code").getAsInt(); int code = result.get("code") == null ? 500 : result.get("code").getAsInt();
if(code == 205) { 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) { } else if(code == 401) {
throw new CardAuthorizationException("Incorrect PIN provided."); throw new CardAuthorizationException("Incorrect PIN provided");
} }
throw new CardException(code + " on " + cmd + ": " + msg); throw new CardException(code + " on " + cmd + ": " + msg);

View file

@ -88,6 +88,7 @@ public class CkCard implements KeystoreCardImport {
return WalletModel.TAPSIGNER; return WalletModel.TAPSIGNER;
} }
@Override
public StringProperty messageProperty() { public StringProperty messageProperty() {
return messageProperty; return messageProperty;
} }

View file

@ -754,8 +754,13 @@ public class HeadersController extends TransactionFormController implements Init
Optional<Keystore> softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny(); Optional<Keystore> softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny();
Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)).findAny(); Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)).findAny();
Optional<Keystore> bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny(); Optional<Keystore> bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny();
if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty()) { Optional<Keystore> cardKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getWalletModel().equals(WalletModel.TAPSIGNER)).findAny();
if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty() && cardKeystore.isEmpty()) {
signButton.setDisable(true); 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()) { } else if(softwareKeystore.isEmpty() && bip47Keystore.isEmpty()) {
Glyph usbGlyph = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB); Glyph usbGlyph = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
usbGlyph.setFontSize(20); usbGlyph.setFontSize(20);
@ -935,7 +940,7 @@ public class HeadersController extends TransactionFormController implements Init
public void signPSBT(ActionEvent event) { public void signPSBT(ActionEvent event) {
signSoftwareKeystores(); signSoftwareKeystores();
signUsbKeystores(); signDeviceKeystores();
} }
private void signSoftwareKeystores() { private void signSoftwareKeystores() {
@ -982,7 +987,7 @@ public class HeadersController extends TransactionFormController implements Init
} }
} }
private void signUsbKeystores() { private void signDeviceKeystores() {
if(headersForm.getPsbt().isSigned()) { if(headersForm.getPsbt().isSigned()) {
return; return;
} }
@ -990,12 +995,12 @@ public class HeadersController extends TransactionFormController implements Init
List<String> fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList()); List<String> fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList());
List<Device> signingDevices = AppServices.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList()); List<Device> signingDevices = AppServices.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList());
if(signingDevices.isEmpty() && 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))))) { (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; return;
} }
DeviceSignDialog dlg = new DeviceSignDialog(fingerprints, headersForm.getPsbt()); DeviceSignDialog dlg = new DeviceSignDialog(headersForm.getSigningWallet(), fingerprints, headersForm.getPsbt());
dlg.initModality(Modality.NONE); dlg.initModality(Modality.NONE);
Stage stage = (Stage)dlg.getDialogPane().getScene().getWindow(); Stage stage = (Stage)dlg.getDialogPane().getScene().getWindow();
stage.setAlwaysOnTop(true); stage.setAlwaysOnTop(true);