From 6c13504644075045cc61d908196897d8d3144c9b Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 26 Jan 2023 15:47:33 +0200 Subject: [PATCH] implement card initialization functionality --- drongo | 2 +- .../sparrow/control/CardImportPane.java | 109 ++++++++++++++++-- .../control/TitledDescriptionPane.java | 8 +- .../sparrowwallet/sparrow/io/CardImport.java | 4 + .../sparrow/io/ckcard/CardApi.java | 23 +++- .../sparrow/io/ckcard/CardProtocol.java | 12 +- .../sparrow/io/ckcard/CardStatus.java | 7 +- .../sparrow/io/ckcard/CardTransport.java | 5 +- .../sparrow/io/ckcard/CkCard.java | 35 +++++- 9 files changed, 175 insertions(+), 30 deletions(-) diff --git a/drongo b/drongo index 2168c56d..a14b23f2 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 2168c56de924dd7bff5458b32f9a60a6c79066a1 +Subproject commit a14b23f2fabc35c1c0b4b7b9f886dab10b4f7562 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java index 6afa5f87..9f0f005f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java @@ -3,8 +3,10 @@ package com.sparrowwallet.sparrow.control; import com.google.common.base.Throwables; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; @@ -16,8 +18,7 @@ import javafx.concurrent.Task; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Control; +import javafx.scene.control.*; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -26,6 +27,8 @@ import org.controlsfx.glyphfont.Glyph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.smartcardio.CardException; +import java.nio.charset.StandardCharsets; import java.util.List; public class CardImportPane extends TitledDescriptionPane { @@ -36,6 +39,7 @@ public class CardImportPane extends TitledDescriptionPane { 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"); @@ -57,6 +61,22 @@ public class CardImportPane extends TitledDescriptionPane { } private void importCard() { + errorText.set(""); + + try { + if(!importer.isInitialized()) { + setDescription("Card not initialized"); + setContent(getInitializationPanel()); + setExpanded(true); + return; + } else { + initialized = true; + } + } catch(CardException e) { + setError("Card Error", e.getMessage()); + return; + } + if(pin.get().isEmpty()) { setDescription("Enter PIN code"); setContent(getPinEntry()); @@ -85,21 +105,71 @@ public class CardImportPane extends TitledDescriptionPane { @Override protected void setError(String title, String detail) { - super.setError(title, null); - errorText.set(detail); - setContent(getPinEntry()); - setExpanded(true); + 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)"); + 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)); + 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()); + 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 getPinEntry() { VBox vBox = new VBox(); if(!errorText.get().isEmpty()) { - Node contextBox = getContentBox(errorText.get()); - if(contextBox instanceof HBox hBox && hBox.getPrefHeight() == 60) { + Node errorBox = getContentBox(errorText.get()); + if(errorBox instanceof HBox hBox && hBox.getPrefHeight() == 60) { hBox.setPrefHeight(50); } - vBox.getChildren().add(contextBox); + vBox.getChildren().add(errorBox); } CustomPasswordField pinField = new ViewPasswordField(); @@ -121,6 +191,27 @@ public class CardImportPane extends TitledDescriptionPane { return vBox; } + public static class CardInitializationService extends Service { + private final KeystoreCardImport cardImport; + private final byte[] chainCode; + + public CardInitializationService(KeystoreCardImport cardImport, byte[] chainCode) { + this.cardImport = cardImport; + this.chainCode = chainCode; + } + + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Void call() throws Exception { + cardImport.initialize(chainCode); + return null; + } + }; + } + } + public static class CardImportService extends Service { private final KeystoreCardImport cardImport; private final String pin; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java b/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java index b87017d6..3f2f1262 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TitledDescriptionPane.java @@ -108,13 +108,17 @@ public class TitledDescriptionPane extends TitledPane { protected void setDescription(String text) { descriptionLabel.getStyleClass().remove("description-error"); - descriptionLabel.getStyleClass().add("description-label"); + if(!descriptionLabel.getStyleClass().contains("description-label")) { + descriptionLabel.getStyleClass().add("description-label"); + } descriptionLabel.setText(text); } protected void setError(String title, String detail) { descriptionLabel.getStyleClass().remove("description-label"); - descriptionLabel.getStyleClass().add("description-error"); + if(!descriptionLabel.getStyleClass().contains("description-error")) { + descriptionLabel.getStyleClass().add("description-error"); + } descriptionLabel.setText(title); if(detail != null && !detail.isEmpty()) { setContent(getContentBox(detail)); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java b/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java index f6a2aaa4..58f3c1f3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java @@ -2,6 +2,10 @@ package com.sparrowwallet.sparrow.io; import javafx.beans.property.StringProperty; +import javax.smartcardio.CardException; + public interface CardImport extends ImportExport { + boolean isInitialized() throws CardException; + void initialize(byte[] chainCode) throws CardException; StringProperty messageProperty(); } 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 5d94ce62..c93adb55 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java @@ -23,21 +23,36 @@ import java.util.List; public class CardApi { private static final Logger log = LoggerFactory.getLogger(CardApi.class); + private final WalletModel cardType; private final CardProtocol cardProtocol; private String cvc; public CardApi(String cvc) throws CardException { + this(WalletModel.TAPSIGNER, cvc); + } + + public CardApi(WalletModel cardType, String cvc) throws CardException { + this.cardType = cardType; this.cardProtocol = new CardProtocol(); this.cvc = cvc; } - public void initialize() throws CardException { + public boolean isInitialized() throws CardException { + CardStatus cardStatus = getStatus(); + return cardStatus.isInitialized(); + } + + public void initialize(byte[] chainCode) throws CardException { cardProtocol.verify(); - cardProtocol.setup(cvc, null); + cardProtocol.setup(cvc, chainCode); } CardStatus getStatus() throws CardException { - return cardProtocol.getStatus(); + CardStatus cardStatus = cardProtocol.getStatus(); + if(cardStatus.getCardType() != cardType) { + throw new CardException("Please use a " + cardType.toDisplayString() + " card."); + } + return cardStatus; } void checkWait(CardStatus cardStatus, IntegerProperty delayProperty, StringProperty messageProperty) throws CardException { @@ -91,7 +106,7 @@ public class CardApi { } public Keystore getKeystore() throws CardException { - CardStatus cardStatus = cardProtocol.getStatus(); + CardStatus cardStatus = getStatus(); CardXpub masterXpub = cardProtocol.xpub(cvc, true); ExtendedKey masterXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(masterXpub.xpub)); 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 55da7e66..304d91b7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java @@ -94,17 +94,17 @@ public class CardProtocol { return gson.fromJson(read, CardRead.class); } - public CardSetup setup(String cvc, byte[] entropy) throws CardException { - if(entropy == null) { - entropy = Sha256Hash.hashTwice(secureRandom.generateSeed(128)); + public CardSetup setup(String cvc, byte[] chainCode) throws CardException { + if(chainCode == null) { + chainCode = Sha256Hash.hashTwice(secureRandom.generateSeed(128)); } - if(entropy.length != 32) { - throw new IllegalArgumentException("Invalid entropy length of " + entropy.length); + if(chainCode.length != 32) { + throw new IllegalArgumentException("Invalid chain code length of " + chainCode.length); } Map args = new HashMap<>(); - args.put("chain_code", entropy); + args.put("chain_code", chainCode); JsonObject setup = sendAuth("new", args, cvc); return gson.fromJson(setup, CardSetup.class); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardStatus.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardStatus.java index 857f4661..26f6e7e0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardStatus.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardStatus.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io.ckcard; import com.google.common.io.BaseEncoding; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.wallet.WalletModel; import java.math.BigInteger; import java.util.Arrays; @@ -13,7 +14,7 @@ public class CardStatus extends CardResponse { int proto; String ver; BigInteger birth; - Boolean tapsigner; + boolean tapsigner; List path; BigInteger num_backups; byte[] pubkey; @@ -42,6 +43,10 @@ public class CardStatus extends CardResponse { return num_backups == null || num_backups.intValue() == 0; } + public WalletModel getCardType() { + return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD; + } + @Override public String toString() { return "CardStatus{" + diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java index 0dd67552..b2de7f83 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java @@ -45,7 +45,8 @@ public class CardTransport { CardChannel cardChannel = connection.getBasicChannel(); ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase()))); if(resp.getSW() != SW_OKAY) { - throw new CardException("Card returned response of " + resp.getSW()); + log.error("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW())); + throw new CardException("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()) + ". Note that only the Tapsigner is currently supported."); } } @@ -119,7 +120,7 @@ public class CardTransport { } } } catch(CborException e) { - e.printStackTrace(); + log.error("CBOR encoding error", e); } return new JsonObject(); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java index cc5df767..478b89ad 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCard.java @@ -15,14 +15,40 @@ import java.util.List; public class CkCard implements KeystoreCardImport { private final StringProperty messageProperty = new SimpleStringProperty(""); + @Override + public boolean isInitialized() throws CardException { + CardApi cardApi = null; + try { + cardApi = new CardApi(null); + return cardApi.isInitialized(); + } finally { + if(cardApi != null) { + cardApi.disconnect(); + } + } + } + + @Override + public void initialize(byte[] chainCode) throws CardException { + CardApi cardApi = null; + try { + cardApi = new CardApi(null); + cardApi.initialize(chainCode); + } finally { + if(cardApi != null) { + cardApi.disconnect(); + } + } + } + @Override public Keystore getKeystore(String pin, List derivation) throws ImportException { if(pin.length() < 6) { - throw new ImportException("PIN too short"); + throw new ImportException("PIN too short."); } if(pin.length() > 32) { - throw new ImportException("PIN too long"); + throw new ImportException("PIN too long."); } CardApi cardApi = null; @@ -30,8 +56,7 @@ public class CkCard implements KeystoreCardImport { cardApi = new CardApi(pin); CardStatus cardStatus = cardApi.getStatus(); if(!cardStatus.isInitialized()) { - cardApi.initialize(); - cardStatus = cardApi.getStatus(); + throw new IllegalStateException("Card is not initialized."); } cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); @@ -39,7 +64,7 @@ public class CkCard implements KeystoreCardImport { cardApi.setDerivation(derivation); } return cardApi.getKeystore(); - } catch(CardException e) { + } catch(Exception e) { throw new ImportException(e); } finally { if(cardApi != null) {