mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-05 05:46:44 +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;
|
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);
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue