implement card initialization functionality

This commit is contained in:
Craig Raw 2023-01-26 15:47:33 +02:00
parent 3ddf4ed4b2
commit 6c13504644
9 changed files with 175 additions and 30 deletions

2
drongo

@ -1 +1 @@
Subproject commit 2168c56de924dd7bff5458b32f9a60a6c79066a1 Subproject commit a14b23f2fabc35c1c0b4b7b9f886dab10b4f7562

View file

@ -3,8 +3,10 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -16,8 +18,7 @@ import javafx.concurrent.Task;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.*;
import javafx.scene.control.Control;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@ -26,6 +27,8 @@ import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
public class CardImportPane extends TitledDescriptionPane { public class CardImportPane extends TitledDescriptionPane {
@ -36,6 +39,7 @@ public class CardImportPane extends TitledDescriptionPane {
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 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(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
@ -57,6 +61,22 @@ public class CardImportPane extends TitledDescriptionPane {
} }
private void importCard() { 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()) { if(pin.get().isEmpty()) {
setDescription("Enter PIN code"); setDescription("Enter PIN code");
setContent(getPinEntry()); setContent(getPinEntry());
@ -85,21 +105,71 @@ public class CardImportPane extends TitledDescriptionPane {
@Override @Override
protected void setError(String title, String detail) { protected void setError(String title, String detail) {
super.setError(title, null); if(!initialized) {
errorText.set(detail); super.setError(title, detail);
setContent(getPinEntry()); } else {
setExpanded(true); 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() { private Node getPinEntry() {
VBox vBox = new VBox(); VBox vBox = new VBox();
if(!errorText.get().isEmpty()) { if(!errorText.get().isEmpty()) {
Node contextBox = getContentBox(errorText.get()); Node errorBox = getContentBox(errorText.get());
if(contextBox instanceof HBox hBox && hBox.getPrefHeight() == 60) { if(errorBox instanceof HBox hBox && hBox.getPrefHeight() == 60) {
hBox.setPrefHeight(50); hBox.setPrefHeight(50);
} }
vBox.getChildren().add(contextBox); vBox.getChildren().add(errorBox);
} }
CustomPasswordField pinField = new ViewPasswordField(); CustomPasswordField pinField = new ViewPasswordField();
@ -121,6 +191,27 @@ public class CardImportPane extends TitledDescriptionPane {
return vBox; return vBox;
} }
public static class CardInitializationService extends Service<Void> {
private final KeystoreCardImport cardImport;
private final byte[] chainCode;
public CardInitializationService(KeystoreCardImport cardImport, byte[] chainCode) {
this.cardImport = cardImport;
this.chainCode = chainCode;
}
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() throws Exception {
cardImport.initialize(chainCode);
return null;
}
};
}
}
public static class CardImportService extends Service<Keystore> { public static class CardImportService extends Service<Keystore> {
private final KeystoreCardImport cardImport; private final KeystoreCardImport cardImport;
private final String pin; private final String pin;

View file

@ -108,13 +108,17 @@ public class TitledDescriptionPane extends TitledPane {
protected void setDescription(String text) { protected void setDescription(String text) {
descriptionLabel.getStyleClass().remove("description-error"); descriptionLabel.getStyleClass().remove("description-error");
descriptionLabel.getStyleClass().add("description-label"); if(!descriptionLabel.getStyleClass().contains("description-label")) {
descriptionLabel.getStyleClass().add("description-label");
}
descriptionLabel.setText(text); descriptionLabel.setText(text);
} }
protected void setError(String title, String detail) { protected void setError(String title, String detail) {
descriptionLabel.getStyleClass().remove("description-label"); descriptionLabel.getStyleClass().remove("description-label");
descriptionLabel.getStyleClass().add("description-error"); if(!descriptionLabel.getStyleClass().contains("description-error")) {
descriptionLabel.getStyleClass().add("description-error");
}
descriptionLabel.setText(title); descriptionLabel.setText(title);
if(detail != null && !detail.isEmpty()) { if(detail != null && !detail.isEmpty()) {
setContent(getContentBox(detail)); setContent(getContentBox(detail));

View file

@ -2,6 +2,10 @@ package com.sparrowwallet.sparrow.io;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javax.smartcardio.CardException;
public interface CardImport extends ImportExport { public interface CardImport extends ImportExport {
boolean isInitialized() throws CardException;
void initialize(byte[] chainCode) throws CardException;
StringProperty messageProperty(); StringProperty messageProperty();
} }

View file

@ -23,21 +23,36 @@ import java.util.List;
public class CardApi { public class CardApi {
private static final Logger log = LoggerFactory.getLogger(CardApi.class); private static final Logger log = LoggerFactory.getLogger(CardApi.class);
private final WalletModel cardType;
private final CardProtocol cardProtocol; private final CardProtocol cardProtocol;
private String cvc; private String cvc;
public CardApi(String cvc) throws CardException { 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.cardProtocol = new CardProtocol();
this.cvc = cvc; 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.verify();
cardProtocol.setup(cvc, null); cardProtocol.setup(cvc, chainCode);
} }
CardStatus getStatus() throws CardException { 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 { void checkWait(CardStatus cardStatus, IntegerProperty delayProperty, StringProperty messageProperty) throws CardException {
@ -91,7 +106,7 @@ public class CardApi {
} }
public Keystore getKeystore() throws CardException { public Keystore getKeystore() throws CardException {
CardStatus cardStatus = cardProtocol.getStatus(); CardStatus cardStatus = getStatus();
CardXpub masterXpub = cardProtocol.xpub(cvc, true); CardXpub masterXpub = cardProtocol.xpub(cvc, true);
ExtendedKey masterXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(masterXpub.xpub)); ExtendedKey masterXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(masterXpub.xpub));

View file

@ -94,17 +94,17 @@ public class CardProtocol {
return gson.fromJson(read, CardRead.class); return gson.fromJson(read, CardRead.class);
} }
public CardSetup setup(String cvc, byte[] entropy) throws CardException { public CardSetup setup(String cvc, byte[] chainCode) throws CardException {
if(entropy == null) { if(chainCode == null) {
entropy = Sha256Hash.hashTwice(secureRandom.generateSeed(128)); chainCode = Sha256Hash.hashTwice(secureRandom.generateSeed(128));
} }
if(entropy.length != 32) { if(chainCode.length != 32) {
throw new IllegalArgumentException("Invalid entropy length of " + entropy.length); throw new IllegalArgumentException("Invalid chain code length of " + chainCode.length);
} }
Map<String, Object> args = new HashMap<>(); Map<String, Object> args = new HashMap<>();
args.put("chain_code", entropy); args.put("chain_code", chainCode);
JsonObject setup = sendAuth("new", args, cvc); JsonObject setup = sendAuth("new", args, cvc);
return gson.fromJson(setup, CardSetup.class); return gson.fromJson(setup, CardSetup.class);
} }

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io.ckcard;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
@ -13,7 +14,7 @@ public class CardStatus extends CardResponse {
int proto; int proto;
String ver; String ver;
BigInteger birth; BigInteger birth;
Boolean tapsigner; boolean tapsigner;
List<BigInteger> path; List<BigInteger> path;
BigInteger num_backups; BigInteger num_backups;
byte[] pubkey; byte[] pubkey;
@ -42,6 +43,10 @@ public class CardStatus extends CardResponse {
return num_backups == null || num_backups.intValue() == 0; return num_backups == null || num_backups.intValue() == 0;
} }
public WalletModel getCardType() {
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
}
@Override @Override
public String toString() { public String toString() {
return "CardStatus{" + return "CardStatus{" +

View file

@ -45,7 +45,8 @@ public class CardTransport {
CardChannel cardChannel = connection.getBasicChannel(); CardChannel cardChannel = connection.getBasicChannel();
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase()))); ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase())));
if(resp.getSW() != SW_OKAY) { 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) { } catch(CborException e) {
e.printStackTrace(); log.error("CBOR encoding error", e);
} }
return new JsonObject(); return new JsonObject();

View file

@ -15,14 +15,40 @@ import java.util.List;
public class CkCard implements KeystoreCardImport { public class CkCard implements KeystoreCardImport {
private final StringProperty messageProperty = new SimpleStringProperty(""); 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 @Override
public Keystore getKeystore(String pin, List<ChildNumber> derivation) throws ImportException { public Keystore getKeystore(String pin, List<ChildNumber> derivation) throws ImportException {
if(pin.length() < 6) { if(pin.length() < 6) {
throw new ImportException("PIN too short"); throw new ImportException("PIN too short.");
} }
if(pin.length() > 32) { if(pin.length() > 32) {
throw new ImportException("PIN too long"); throw new ImportException("PIN too long.");
} }
CardApi cardApi = null; CardApi cardApi = null;
@ -30,8 +56,7 @@ public class CkCard implements KeystoreCardImport {
cardApi = new CardApi(pin); cardApi = new CardApi(pin);
CardStatus cardStatus = cardApi.getStatus(); CardStatus cardStatus = cardApi.getStatus();
if(!cardStatus.isInitialized()) { if(!cardStatus.isInitialized()) {
cardApi.initialize(); throw new IllegalStateException("Card is not initialized.");
cardStatus = cardApi.getStatus();
} }
cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
@ -39,7 +64,7 @@ public class CkCard implements KeystoreCardImport {
cardApi.setDerivation(derivation); cardApi.setDerivation(derivation);
} }
return cardApi.getKeystore(); return cardApi.getKeystore();
} catch(CardException e) { } catch(Exception e) {
throw new ImportException(e); throw new ImportException(e);
} finally { } finally {
if(cardApi != null) { if(cardApi != null) {