add card scan to hwi enumeration and refactor device pane

This commit is contained in:
Craig Raw 2023-01-27 13:58:38 +02:00
parent 7a99c4a11a
commit 4fb8c5a61b
7 changed files with 173 additions and 72 deletions

View file

@ -14,6 +14,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog; import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.ckcard.CardApi;
import com.sparrowwallet.sparrow.net.Auth47; import com.sparrowwallet.sparrow.net.Auth47;
import com.sparrowwallet.drongo.protocol.BlockHeader; import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
@ -1112,7 +1113,7 @@ public class AppServices {
Wallet wallet = walletTabData.getWallet(); Wallet wallet = walletTabData.getWallet();
Storage storage = walletTabData.getStorage(); Storage storage = walletTabData.getStorage();
if(Interface.get() == Interface.DESKTOP && (!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB))) { if(Interface.get() == Interface.DESKTOP && (!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB) || CardApi.isReaderAvailable())) {
usbWallet = true; usbWallet = true;
if(deviceEnumerateService == null) { if(deviceEnumerateService == null) {

View file

@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.KeystoreCardImport; import com.sparrowwallet.sparrow.io.KeystoreCardImport;
import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException; import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Service; import javafx.concurrent.Service;
import javafx.concurrent.Task; import javafx.concurrent.Task;
@ -153,6 +154,7 @@ public class CardImportPane extends TitledDescriptionPane {
importButton.setDefaultButton(true); importButton.setDefaultButton(true);
pin.bind(pinField.textProperty()); pin.bind(pinField.textProperty());
HBox.setHgrow(pinField, Priority.ALWAYS); HBox.setHgrow(pinField, Priority.ALWAYS);
Platform.runLater(pinField::requestFocus);
HBox contentBox = new HBox(); HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT); contentBox.setAlignment(Pos.TOP_RIGHT);

View file

@ -8,20 +8,26 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.Policy; 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.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
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.KeystoreCardImport;
import com.sparrowwallet.sparrow.io.ckcard.CardApi; import com.sparrowwallet.sparrow.io.ckcard.CardApi;
import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException; import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException;
import com.sparrowwallet.sparrow.io.ckcard.CkCard;
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.beans.property.StringProperty;
import javafx.concurrent.Service; import javafx.concurrent.Service;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
@ -36,6 +42,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -562,7 +569,27 @@ public class DevicePane extends TitledDescriptionPane {
} }
private void importKeystore(List<ChildNumber> derivation) { private void importKeystore(List<ChildNumber> derivation) {
if(device.getFingerprint() == null) { if(device.isCard()) {
try {
CkCard importer = new CkCard();
if(!importer.isInitialized()) {
setDescription("Card not initialized");
setContent(getCardInitializationPanel(importer));
showHideLink.setVisible(false);
setExpanded(true);
return;
}
Service<Keystore> importService = new CardImportPane.CardImportService(importer, pin.get(), derivation);
handleCardOperation(importService, importButton, "Import", event -> {
importKeystore(derivation, importService.getValue());
});
} catch(Exception e) {
log.error("Import Error: " + e.getMessage(), e);
setError("Import Error", e.getMessage());
importButton.setDisable(false);
}
} else if(device.getFingerprint() == null) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get()); Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
enumerateService.setOnSucceeded(workerStateEvent -> { enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue(); List<Device> devices = enumerateService.getValue();
@ -599,18 +626,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath)); keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub)); keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
if(wallet.getScriptType() == null) { importKeystore(derivation, keystore);
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
} catch(Exception e) { } catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage()); setError("Could not retrieve xpub", e.getMessage());
} }
@ -624,35 +640,29 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.start(); getXpubService.start();
} }
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
private void sign() { private void sign() {
if(device.isCard()) { 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 { try {
CardApi cardApi = new CardApi(pin.get()); CardApi cardApi = new CardApi(pin.get());
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty);
Service<Void> signService = cardApi.getSignService(wallet, psbt, messageProperty); handleCardOperation(signService, signButton, "Signing", event -> {
signService.setOnSucceeded(event -> { EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
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) { } catch(Exception e) {
log.error("Signing Error: " + e.getMessage(), e); log.error("Signing Error: " + e.getMessage(), e);
setError("Signing Error", e.getMessage()); setError("Signing Error", e.getMessage());
@ -675,6 +685,31 @@ public class DevicePane extends TitledDescriptionPane {
} }
} }
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, EventHandler<WorkerStateEvent> successHandler) {
if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(operationButton));
showHideLink.setVisible(false);
setExpanded(true);
operationButton.setDisable(false);
return;
}
service.setOnSucceeded(successHandler);
service.setOnFailed(event -> {
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getCardPinEntry(operationButton));
} else {
log.error(operationDescription + " Error: " + rootCause.getMessage(), event.getSource().getException());
setError(operationDescription + " Error", rootCause.getMessage());
}
operationButton.setDisable(false);
});
service.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);
displayAddressService.setOnSucceeded(successEvent -> { displayAddressService.setOnSucceeded(successEvent -> {
@ -793,7 +828,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField(); TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path"); derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation)); derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(keyDerivation != null); derivationField.setDisable(device.isCard() || keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS); HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport(); ValidationSupport validationSupport = new ValidationSupport();
@ -828,14 +863,63 @@ public class DevicePane extends TitledDescriptionPane {
return contentBox; return contentBox;
} }
private Node getCardPinEntry() { private Node getCardInitializationPanel(KeystoreCardImport importer) {
VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField();
entropy.setPromptText("Enter input for chain code");
entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup();
automatic.setToggleGroup(toggleGroup);
advanced.setToggleGroup(toggleGroup);
automatic.setSelected(true);
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
entropy.setDisable(newValue == automatic);
});
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
CardImportPane.CardInitializationService cardInitializationService = new CardImportPane.CardInitializationService(importer, chainCode);
cardInitializationService.setOnSucceeded(event1 -> {
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(getCardPinEntry(importButton));
importButton.setDisable(false);
setExpanded(true);
});
cardInitializationService.setOnFailed(event1 -> {
Throwable e = event1.getSource().getException();
log.error("Error initializing card", e);
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + e.getMessage());
});
cardInitializationService.start();
});
HBox contentBox = new HBox(20);
contentBox.getChildren().addAll(initTypeBox, initializeButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
return contentBox;
}
private Node getCardPinEntry(ButtonBase operationButton) {
VBox vBox = new VBox(); VBox vBox = new VBox();
CustomPasswordField pinField = new ViewPasswordField(); CustomPasswordField pinField = new ViewPasswordField();
pinField.setPromptText("PIN Code"); pinField.setPromptText("PIN Code");
signButton.setDefaultButton(true); if(operationButton instanceof Button defaultButton) {
defaultButton.setDefaultButton(true);
}
pin.bind(pinField.textProperty()); pin.bind(pinField.textProperty());
HBox.setHgrow(pinField, Priority.ALWAYS); HBox.setHgrow(pinField, Priority.ALWAYS);
Platform.runLater(pinField::requestFocus);
HBox contentBox = new HBox(); HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT); contentBox.setAlignment(Pos.TOP_RIGHT);

View file

@ -32,31 +32,6 @@ 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(wallet, psbt, device, defaultDevice); return new DevicePane(wallet, psbt, device, defaultDevice);

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.io.ckcard.CardApi;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service; import javafx.concurrent.Service;
import javafx.concurrent.Task; import javafx.concurrent.Task;
@ -17,6 +18,8 @@ import org.controlsfx.tools.Platform;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import javax.smartcardio.CardNotPresentException;
import java.io.*; import java.io.*;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -40,6 +43,13 @@ public class Hwi {
private static boolean isPromptActive = false; private static boolean isPromptActive = false;
public List<Device> enumerate(String passphrase) throws ImportException { public List<Device> enumerate(String passphrase) throws ImportException {
List<Device> devices = new ArrayList<>();
devices.addAll(enumerateUsb(passphrase));
devices.addAll(enumerateCard());
return devices;
}
private List<Device> enumerateUsb(String passphrase) throws ImportException {
String output = null; String output = null;
try { try {
List<String> command; List<String> command;
@ -84,6 +94,30 @@ public class Hwi {
} }
} }
private List<Device> enumerateCard() {
List<Device> devices = new ArrayList<>();
if(CardApi.isReaderAvailable()) {
try {
CardApi cardApi = new CardApi(null);
WalletModel walletModel = cardApi.getCardType();
Device cardDevice = new Device();
cardDevice.setType(walletModel.getType());
cardDevice.setModel(walletModel);
cardDevice.setNeedsPassphraseSent(Boolean.FALSE);
cardDevice.setNeedsPinSent(Boolean.FALSE);
cardDevice.setCard(true);
devices.add(cardDevice);
} catch(CardNotPresentException e) {
//ignore
} catch(CardException e) {
log.error("Error reading card", e);
}
}
return devices;
}
public boolean promptPin(Device device) throws ImportException { public boolean promptPin(Device device) throws ImportException {
try { try {
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN)); String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));

View file

@ -54,6 +54,11 @@ public class CardApi {
cardProtocol.setup(cvc, chainCode); cardProtocol.setup(cvc, chainCode);
} }
public WalletModel getCardType() throws CardException {
CardStatus cardStatus = getStatus();
return cardStatus.getCardType();
}
CardStatus getStatus() throws CardException { CardStatus getStatus() throws CardException {
CardStatus cardStatus = cardProtocol.getStatus(); CardStatus cardStatus = cardProtocol.getStatus();
if(cardStatus.getCardType() != cardType) { if(cardStatus.getCardType() != cardType) {
@ -139,7 +144,7 @@ public class CardApi {
return Utils.bytesToHex(masterXpubkey.getKey().getFingerprint()); return Utils.bytesToHex(masterXpubkey.getKey().getFingerprint());
} }
public Service<Void> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) { public Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
return new SignService(wallet, psbt, messageProperty); return new SignService(wallet, psbt, messageProperty);
} }
@ -230,7 +235,7 @@ public class CardApi {
} }
} }
public class SignService extends Service<Void> { public class SignService extends Service<PSBT> {
private final Wallet wallet; private final Wallet wallet;
private final PSBT psbt; private final PSBT psbt;
private final StringProperty messageProperty; private final StringProperty messageProperty;
@ -242,15 +247,15 @@ public class CardApi {
} }
@Override @Override
protected Task<Void> createTask() { protected Task<PSBT> createTask() {
return new Task<>() { return new Task<>() {
@Override @Override
protected Void call() throws Exception { protected PSBT call() throws Exception {
CardStatus cardStatus = getStatus(); CardStatus cardStatus = getStatus();
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
sign(wallet, psbt); sign(wallet, psbt);
return null; return psbt;
} }
}; };
} }

View file

@ -167,7 +167,7 @@ public class CardProtocol {
} }
} }
throw new CardSignFailedException("Failed to sign digest after 5 tries."); throw new CardSignFailedException("Failed to sign digest after 5 tries. It's safe to try again.");
} }
public CardChange change(String currentCvc, String newCvc) throws CardException { public CardChange change(String currentCvc, String newCvc) throws CardException {