From 4fb8c5a61b3380260c4d32e9ee4bf4408a1571ec Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 27 Jan 2023 13:58:38 +0200 Subject: [PATCH] add card scan to hwi enumeration and refactor device pane --- .../sparrowwallet/sparrow/AppServices.java | 3 +- .../sparrow/control/CardImportPane.java | 2 + .../sparrow/control/DevicePane.java | 164 +++++++++++++----- .../sparrow/control/DeviceSignDialog.java | 25 --- .../com/sparrowwallet/sparrow/io/Hwi.java | 34 ++++ .../sparrow/io/ckcard/CardApi.java | 15 +- .../sparrow/io/ckcard/CardProtocol.java | 2 +- 7 files changed, 173 insertions(+), 72 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 15e464dc..7cb39f57 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -14,6 +14,7 @@ import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.control.WalletPasswordDialog; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.ckcard.CardApi; import com.sparrowwallet.sparrow.net.Auth47; import com.sparrowwallet.drongo.protocol.BlockHeader; import com.sparrowwallet.drongo.protocol.ScriptType; @@ -1112,7 +1113,7 @@ public class AppServices { Wallet wallet = walletTabData.getWallet(); 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; if(deviceEnumerateService == null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java index e2c2adcd..b69ee6ce 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java @@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.KeystoreCardImport; import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException; +import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.concurrent.Service; import javafx.concurrent.Task; @@ -153,6 +154,7 @@ public class CardImportPane extends TitledDescriptionPane { importButton.setDefaultButton(true); pin.bind(pinField.textProperty()); HBox.setHgrow(pinField, Priority.ALWAYS); + Platform.runLater(pinField::requestFocus); HBox contentBox = new HBox(); contentBox.setAlignment(Pos.TOP_RIGHT); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index 6068fa35..ca0860fc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -8,20 +8,26 @@ import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.sparrow.AppServices; 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.KeystoreCardImport; import com.sparrowwallet.sparrow.io.ckcard.CardApi; import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException; +import com.sparrowwallet.sparrow.io.ckcard.CkCard; 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.concurrent.WorkerStateEvent; +import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -36,6 +42,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; @@ -562,7 +569,27 @@ public class DevicePane extends TitledDescriptionPane { } private void importKeystore(List 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 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()); enumerateService.setOnSucceeded(workerStateEvent -> { List devices = enumerateService.getValue(); @@ -599,18 +626,7 @@ public class DevicePane extends TitledDescriptionPane { keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath)); keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub)); - 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)); - } + importKeystore(derivation, keystore); } catch(Exception e) { setError("Could not retrieve xpub", e.getMessage()); } @@ -624,35 +640,29 @@ public class DevicePane extends TitledDescriptionPane { getXpubService.start(); } + private void importKeystore(List 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() { if(device.isCard()) { - if(pin.get().length() < 6) { - setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); - setContent(getCardPinEntry()); - setExpanded(true); - signButton.setDisable(false); - return; - } - try { CardApi cardApi = new CardApi(pin.get()); - - Service signService = cardApi.getSignService(wallet, psbt, messageProperty); - signService.setOnSucceeded(event -> { - EventManager.get().post(new PSBTSignedEvent(psbt, psbt)); + Service signService = cardApi.getSignService(wallet, psbt, messageProperty); + handleCardOperation(signService, signButton, "Signing", event -> { + EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue())); }); - 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()); @@ -675,6 +685,31 @@ public class DevicePane extends TitledDescriptionPane { } } + private void handleCardOperation(Service service, ButtonBase operationButton, String operationDescription, EventHandler 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() { Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor); displayAddressService.setOnSucceeded(successEvent -> { @@ -793,7 +828,7 @@ public class DevicePane extends TitledDescriptionPane { TextField derivationField = new TextField(); derivationField.setPromptText("Derivation path"); derivationField.setText(KeyDerivation.writePath(derivation)); - derivationField.setDisable(keyDerivation != null); + derivationField.setDisable(device.isCard() || keyDerivation != null); HBox.setHgrow(derivationField, Priority.ALWAYS); ValidationSupport validationSupport = new ValidationSupport(); @@ -828,14 +863,63 @@ public class DevicePane extends TitledDescriptionPane { 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(); CustomPasswordField pinField = new ViewPasswordField(); pinField.setPromptText("PIN Code"); - signButton.setDefaultButton(true); + if(operationButton instanceof Button defaultButton) { + defaultButton.setDefaultButton(true); + } pin.bind(pinField.textProperty()); HBox.setHgrow(pinField, Priority.ALWAYS); + Platform.runLater(pinField::requestFocus); HBox contentBox = new HBox(); contentBox.setAlignment(Pos.TOP_RIGHT); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java index c53758fa..18f39c10 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceSignDialog.java @@ -32,31 +32,6 @@ public class DeviceSignDialog extends DeviceDialog { setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt); } - @Override - protected List getDevices() { - List devices = super.getDevices(); - - if(CardApi.isReaderAvailable()) { - devices = new ArrayList<>(devices); - try { - CardApi cardApi = new CardApi(null); - if(cardApi.isInitialized()) { - Device cardDevice = new Device(); - cardDevice.setType(WalletModel.TAPSIGNER.getType()); - cardDevice.setModel(WalletModel.TAPSIGNER); - cardDevice.setNeedsPassphraseSent(Boolean.FALSE); - cardDevice.setNeedsPinSent(Boolean.FALSE); - cardDevice.setCard(true); - devices.add(cardDevice); - } - } catch(CardException e) { - log.error("Error reading card", e); - } - } - - return devices; - } - @Override protected DevicePane getDevicePane(Device device, boolean defaultDevice) { return new DevicePane(wallet, psbt, device, defaultDevice); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index 99ffff16..a815ebb5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.WalletModel; +import com.sparrowwallet.sparrow.io.ckcard.CardApi; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; @@ -17,6 +18,8 @@ import org.controlsfx.tools.Platform; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.smartcardio.CardException; +import javax.smartcardio.CardNotPresentException; import java.io.*; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; @@ -40,6 +43,13 @@ public class Hwi { private static boolean isPromptActive = false; public List enumerate(String passphrase) throws ImportException { + List devices = new ArrayList<>(); + devices.addAll(enumerateUsb(passphrase)); + devices.addAll(enumerateCard()); + return devices; + } + + private List enumerateUsb(String passphrase) throws ImportException { String output = null; try { List command; @@ -84,6 +94,30 @@ public class Hwi { } } + private List enumerateCard() { + List 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 { try { String output = execute(getDeviceCommand(device, Command.PROMPT_PIN)); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java index aa02ab1f..8020c354 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java @@ -54,6 +54,11 @@ public class CardApi { cardProtocol.setup(cvc, chainCode); } + public WalletModel getCardType() throws CardException { + CardStatus cardStatus = getStatus(); + return cardStatus.getCardType(); + } + CardStatus getStatus() throws CardException { CardStatus cardStatus = cardProtocol.getStatus(); if(cardStatus.getCardType() != cardType) { @@ -139,7 +144,7 @@ public class CardApi { return Utils.bytesToHex(masterXpubkey.getKey().getFingerprint()); } - public Service getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) { + public Service getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) { return new SignService(wallet, psbt, messageProperty); } @@ -230,7 +235,7 @@ public class CardApi { } } - public class SignService extends Service { + public class SignService extends Service { private final Wallet wallet; private final PSBT psbt; private final StringProperty messageProperty; @@ -242,15 +247,15 @@ public class CardApi { } @Override - protected Task createTask() { + protected Task createTask() { return new Task<>() { @Override - protected Void call() throws Exception { + protected PSBT call() throws Exception { CardStatus cardStatus = getStatus(); checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); sign(wallet, psbt); - return null; + return psbt; } }; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java index 304d91b7..beb301e9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java @@ -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 {