mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add tapsigner signing support and refactor card api
This commit is contained in:
parent
6c13504644
commit
7a99c4a11a
11 changed files with 284 additions and 76 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit a14b23f2fabc35c1c0b4b7b9f886dab10b4f7562
|
||||
Subproject commit e2a4c32db317b9e950cfbec822cc8103332d29ff
|
|
@ -38,18 +38,16 @@ public class CardImportPane extends TitledDescriptionPane {
|
|||
private final List<ChildNumber> 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);
|
||||
|
|
|
@ -68,7 +68,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
|||
|
||||
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||
|
||||
List<Device> devices = AppServices.getDevices();
|
||||
List<Device> devices = getDevices();
|
||||
if(devices == null || devices.isEmpty()) {
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
|
@ -96,6 +96,10 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
|||
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
||||
}
|
||||
|
||||
protected List<Device> getDevices() {
|
||||
return AppServices.getDevices();
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
|
|
|
@ -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<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);
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<PSBT> {
|
||||
private static final Logger log = LoggerFactory.getLogger(DeviceSignDialog.class);
|
||||
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
|
||||
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) {
|
||||
public DeviceSignDialog(Wallet wallet, List<String> 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<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
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(psbt, device, defaultDevice);
|
||||
return new DevicePane(wallet, psbt, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<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() {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -88,6 +88,7 @@ public class CkCard implements KeystoreCardImport {
|
|||
return WalletModel.TAPSIGNER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringProperty messageProperty() {
|
||||
return messageProperty;
|
||||
}
|
||||
|
|
|
@ -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> 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();
|
||||
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);
|
||||
} 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<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());
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue