mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add card scan to hwi enumeration and refactor device pane
This commit is contained in:
parent
7a99c4a11a
commit
4fb8c5a61b
7 changed files with 173 additions and 72 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<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());
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Device> 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<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() {
|
||||
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<Void> signService = cardApi.getSignService(wallet, psbt, messageProperty);
|
||||
signService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, psbt));
|
||||
Service<PSBT> 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<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() {
|
||||
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);
|
||||
|
|
|
@ -32,31 +32,6 @@ public class DeviceSignDialog extends DeviceDialog<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
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(wallet, psbt, device, defaultDevice);
|
||||
|
|
|
@ -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<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;
|
||||
try {
|
||||
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 {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));
|
||||
|
|
|
@ -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<Void> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
|
||||
public Service<PSBT> 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<Void> {
|
||||
public class SignService extends Service<PSBT> {
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final StringProperty messageProperty;
|
||||
|
@ -242,15 +247,15 @@ public class CardApi {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
protected Task<PSBT> 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue