mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
implement card initialization functionality
This commit is contained in:
parent
3ddf4ed4b2
commit
6c13504644
9 changed files with 175 additions and 30 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit 2168c56de924dd7bff5458b32f9a60a6c79066a1
|
||||
Subproject commit a14b23f2fabc35c1c0b4b7b9f886dab10b4f7562
|
|
@ -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<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> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final String pin;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<String, Object> 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);
|
||||
}
|
||||
|
|
|
@ -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<BigInteger> 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{" +
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<ChildNumber> 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) {
|
||||
|
|
Loading…
Reference in a new issue