From 4e3491ec646236d5f8e39126d7473d0832ee96ed Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 1 Feb 2023 09:39:49 +0200 Subject: [PATCH] tapsigner and satscard initialization fixes, satscard address and private key retrieval, core address scanning support --- .../sparrow/control/CardImportPane.java | 71 +++++--- .../sparrow/control/CoinTreeTable.java | 2 +- ...g.java => DeviceDisplayAddressDialog.java} | 4 +- .../control/DeviceGetAddressDialog.java | 29 +++ .../sparrow/control/DevicePane.java | 167 +++++++++++++----- .../sparrow/control/DeviceUnsealDialog.java | 12 +- .../control/PrivateKeySweepDialog.java | 37 +++- .../control/WalletBirthDateDialog.java | 8 +- .../sparrow/event/DeviceAddressEvent.java | 15 ++ ...ent.java => DeviceGetPrivateKeyEvent.java} | 4 +- .../com/sparrowwallet/sparrow/io/CardApi.java | 9 +- .../sparrowwallet/sparrow/io/CardImport.java | 4 +- .../sparrow/io/ckcard/CardAuthDump.java | 5 + .../sparrow/io/ckcard/CardDump.java | 26 +++ .../sparrow/io/ckcard/CardProtocol.java | 52 ++++-- .../sparrow/io/ckcard/CardRead.java | 23 ++- .../sparrow/io/ckcard/CardSign.java | 24 +-- .../sparrow/io/ckcard/CardStatus.java | 12 +- .../sparrow/io/ckcard/CardTransport.java | 2 + .../sparrow/io/ckcard/CkCardApi.java | 127 +++++++++++-- .../sparrow/io/ckcard/Tapsigner.java | 19 +- .../sparrow/net/ElectrumServer.java | 9 +- .../sparrow/net/cormorant/Cormorant.java | 8 + .../cormorant/bitcoind/BitcoindClient.java | 9 +- .../sparrow/wallet/KeystoreController.java | 2 +- .../sparrow/wallet/PaymentController.java | 30 ++++ .../sparrow/wallet/ReceiveController.java | 8 +- 27 files changed, 561 insertions(+), 157 deletions(-) rename src/main/java/com/sparrowwallet/sparrow/control/{DeviceAddressDialog.java => DeviceDisplayAddressDialog.java} (87%) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/DeviceGetAddressDialog.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/DeviceAddressEvent.java rename src/main/java/com/sparrowwallet/sparrow/event/{DeviceUnsealedEvent.java => DeviceGetPrivateKeyEvent.java} (78%) create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardAuthDump.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardDump.java diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java index 4b6c658e..74c02fad 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CardImportPane.java @@ -55,30 +55,19 @@ public class CardImportPane extends TitledDescriptionPane { importButton.setGraphic(tapGlyph); importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setOnAction(event -> { + importButton.setDisable(true); importCard(); }); return importButton; } private void importCard() { - try { - if(!importer.isInitialized()) { - setDescription("Card not initialized"); - setContent(getInitializationPanel()); - showHideLink.setVisible(false); - setExpanded(true); - return; - } - } catch(CardException e) { - setError("Card Error", e.getMessage()); - return; - } - if(pin.get().length() < 6) { setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); setContent(getPinEntry()); showHideLink.setVisible(false); setExpanded(true); + importButton.setDisable(false); return; } @@ -86,6 +75,21 @@ public class CardImportPane extends TitledDescriptionPane { messageProperty.addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> setDescription(newValue)); }); + + try { + if(!importer.isInitialized()) { + setDescription("Card not initialized"); + setContent(getInitializationPanel(messageProperty)); + showHideLink.setVisible(false); + setExpanded(true); + return; + } + } catch(CardException e) { + setError("Card Error", e.getMessage()); + importButton.setDisable(false); + return; + } + CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty); cardImportService.setOnSucceeded(event -> { EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue())); @@ -99,16 +103,17 @@ public class CardImportPane extends TitledDescriptionPane { log.error("Error importing keystore from card", event.getSource().getException()); setError("Import Error", rootCause.getMessage()); } + importButton.setDisable(false); }); cardImportService.start(); } - private Node getInitializationPanel() { + private Node getInitializationPanel(StringProperty messageProperty) { 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.setPromptText("Enter input for user entropy"); entropy.setDisable(true); ToggleGroup toggleGroup = new ToggleGroup(); @@ -124,18 +129,26 @@ public class CardImportPane extends TitledDescriptionPane { Button initializeButton = new Button("Initialize"); initializeButton.setDefaultButton(true); initializeButton.setOnAction(event -> { + initializeButton.setDisable(true); byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8)); - CardInitializationService cardInitializationService = new 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(getPinEntry()); - setExpanded(true); + CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), chainCode, messageProperty); + cardInitializationService.setOnSucceeded(successEvent -> { + AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore."); + setDescription("Leave card on reader"); + setExpanded(false); + importButton.setDisable(false); }); - 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.setOnFailed(failEvent -> { + Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException()); + if(rootCause instanceof CardAuthorizationException) { + setError(rootCause.getMessage(), null); + setContent(getPinEntry()); + importButton.setDisable(false); + } else { + log.error("Error initializing card", failEvent.getSource().getException()); + AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage()); + initializeButton.setDisable(false); + } }); cardInitializationService.start(); }); @@ -172,11 +185,15 @@ public class CardImportPane extends TitledDescriptionPane { public static class CardInitializationService extends Service { private final KeystoreCardImport cardImport; + private final String pin; private final byte[] chainCode; + private final StringProperty messageProperty; - public CardInitializationService(KeystoreCardImport cardImport, byte[] chainCode) { + public CardInitializationService(KeystoreCardImport cardImport, String pin, byte[] chainCode, StringProperty messageProperty) { this.cardImport = cardImport; + this.pin = pin; this.chainCode = chainCode; + this.messageProperty = messageProperty; } @Override @@ -184,7 +201,7 @@ public class CardImportPane extends TitledDescriptionPane { return new Task<>() { @Override protected Void call() throws Exception { - cardImport.initialize(chainCode); + cardImport.initialize(pin, chainCode, messageProperty); return null; } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java index 1189fb01..87f68c0f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java @@ -93,7 +93,7 @@ public class CoinTreeTable extends TreeTableView { Hyperlink hyperlink = new Hyperlink(); hyperlink.setTranslateY(30); hyperlink.setOnAction(event -> { - WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate()); + WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false); Optional optDate = dlg.showAndWait(); if(optDate.isPresent()) { Storage storage = AppServices.get().getOpenWallets().get(wallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceAddressDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceDisplayAddressDialog.java similarity index 87% rename from src/main/java/com/sparrowwallet/sparrow/control/DeviceAddressDialog.java rename to src/main/java/com/sparrowwallet/sparrow/control/DeviceDisplayAddressDialog.java index bc24f38a..07068702 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DeviceAddressDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceDisplayAddressDialog.java @@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device; import java.util.stream.Collectors; -public class DeviceAddressDialog extends DeviceDialog { +public class DeviceDisplayAddressDialog extends DeviceDialog { private final Wallet wallet; private final OutputDescriptor outputDescriptor; - public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) { + public DeviceDisplayAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) { super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList())); this.wallet = wallet; this.outputDescriptor = outputDescriptor; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceGetAddressDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceGetAddressDialog.java new file mode 100644 index 00000000..46877243 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceGetAddressDialog.java @@ -0,0 +1,29 @@ +package com.sparrowwallet.sparrow.control; + +import com.google.common.eventbus.Subscribe; +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.DeviceAddressEvent; +import com.sparrowwallet.sparrow.io.Device; + +import java.util.List; + +public class DeviceGetAddressDialog extends DeviceDialog
{ + public DeviceGetAddressDialog(List operationFingerprints) { + super(operationFingerprints); + EventManager.get().register(this); + setOnCloseRequest(event -> { + EventManager.get().unregister(this); + }); + } + + @Override + protected DevicePane getDevicePane(Device device, boolean defaultDevice) { + return new DevicePane(DevicePane.DeviceOperation.GET_ADDRESS, device, defaultDevice); + } + + @Subscribe + public void deviceAddress(DeviceAddressEvent event) { + setResult(event.getAddress()); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index 91a701d9..39350e45 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -4,6 +4,7 @@ import com.google.common.base.Throwables; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.OutputDescriptor; +import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.policy.Policy; @@ -66,7 +67,8 @@ public class DevicePane extends TitledDescriptionPane { private Button displayAddressButton; private Button signMessageButton; private Button discoverKeystoresButton; - private Button unsealButton; + private Button getPrivateKeyButton; + private Button getAddressButton; private final SimpleStringProperty passphrase = new SimpleStringProperty(""); private final SimpleStringProperty pin = new SimpleStringProperty(""); @@ -201,9 +203,9 @@ public class DevicePane extends TitledDescriptionPane { buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton); } - public DevicePane(Device device, boolean defaultDevice) { + public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) { super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); - this.deviceOperation = DeviceOperation.UNSEAL; + this.deviceOperation = deviceOperation; this.wallet = null; this.psbt = null; this.outputDescriptor = null; @@ -216,7 +218,16 @@ public class DevicePane extends TitledDescriptionPane { setDefaultStatus(); showHideLink.setVisible(false); - createUnsealButton(); + Button button; + if(deviceOperation == DeviceOperation.GET_PRIVATE_KEY) { + createGetPrivateKeyButton(); + button = getPrivateKeyButton; + } else if(deviceOperation == DeviceOperation.GET_ADDRESS) { + createGetAddressButton(); + button = getAddressButton; + } else { + throw new UnsupportedOperationException("Cannot construct device pane for operation " + deviceOperation); + } initialise(device); @@ -224,7 +235,7 @@ public class DevicePane extends TitledDescriptionPane { Platform.runLater(() -> setDescription(newValue)); }); - buttonBox.getChildren().add(unsealButton); + buttonBox.getChildren().add(button); } private void initialise(Device device) { @@ -250,7 +261,7 @@ public class DevicePane extends TitledDescriptionPane { } private void setDefaultStatus() { - setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Place card on reader" : "Unlocked"); + setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Leave card on reader" : "Unlocked"); } private void createUnlockButton() { @@ -370,15 +381,26 @@ public class DevicePane extends TitledDescriptionPane { discoverKeystoresButton.setVisible(false); } - private void createUnsealButton() { - unsealButton = new Button("Unseal"); - unsealButton.setAlignment(Pos.CENTER_RIGHT); - unsealButton.setOnAction(event -> { - unsealButton.setDisable(true); - unseal(); + private void createGetPrivateKeyButton() { + getPrivateKeyButton = new Button("Get Private Key"); + getPrivateKeyButton.setAlignment(Pos.CENTER_RIGHT); + getPrivateKeyButton.setOnAction(event -> { + getPrivateKeyButton.setDisable(true); + getPrivateKey(); }); - unsealButton.managedProperty().bind(unsealButton.visibleProperty()); - unsealButton.setVisible(false); + getPrivateKeyButton.managedProperty().bind(getPrivateKeyButton.visibleProperty()); + getPrivateKeyButton.setVisible(false); + } + + private void createGetAddressButton() { + getAddressButton = new Button("Get Address"); + getAddressButton.setAlignment(Pos.CENTER_RIGHT); + getAddressButton.setOnAction(event -> { + getAddressButton.setDisable(true); + getAddress(); + }); + getAddressButton.managedProperty().bind(getAddressButton.visibleProperty()); + getAddressButton.setVisible(false); } private void unlock(Device device) { @@ -618,15 +640,24 @@ public class DevicePane extends TitledDescriptionPane { try { CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); if(!cardApi.isInitialized()) { + if(pin.get().length() < 6) { + setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); + setContent(getCardPinEntry(importButton)); + showHideLink.setVisible(false); + setExpanded(true); + importButton.setDisable(false); + return; + } + setDescription("Card not initialized"); - setContent(getCardInitializationPanel(cardApi)); + setContent(getCardInitializationPanel(cardApi, importButton, DeviceOperation.IMPORT)); showHideLink.setVisible(false); setExpanded(true); return; } Service importService = cardApi.getImportService(derivation, messageProperty); - handleCardOperation(importService, importButton, "Import", event -> { + handleCardOperation(importService, importButton, "Import", true, event -> { importKeystore(derivation, importService.getValue()); }); } catch(Exception e) { @@ -705,7 +736,7 @@ public class DevicePane extends TitledDescriptionPane { try { CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); Service signService = cardApi.getSignService(wallet, psbt, messageProperty); - handleCardOperation(signService, signButton, "Signing", event -> { + handleCardOperation(signService, signButton, "Signing", true, event -> { EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue())); }); } catch(Exception e) { @@ -730,8 +761,8 @@ public class DevicePane extends TitledDescriptionPane { } } - private void handleCardOperation(Service service, ButtonBase operationButton, String operationDescription, EventHandler successHandler) { - if(pin.get().length() < 6) { + private void handleCardOperation(Service service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler successHandler) { + if(pinRequired && pin.get().length() < 6) { setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); setContent(getCardPinEntry(operationButton)); showHideLink.setVisible(false); @@ -774,7 +805,7 @@ public class DevicePane extends TitledDescriptionPane { try { CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); Service signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty); - handleCardOperation(signMessageService, signMessageButton, "Signing", event -> { + handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> { String signature = signMessageService.getValue(); EventManager.get().post(new MessageSignedEvent(wallet, signature)); }); @@ -855,18 +886,51 @@ public class DevicePane extends TitledDescriptionPane { getXpubsService.start(); } - private void unseal() { + private void getPrivateKey() { if(device.isCard()) { try { CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); - Service unsealService = cardApi.getUnsealService(messageProperty); - handleCardOperation(unsealService, unsealButton, "Unseal", event -> { - EventManager.get().post(new DeviceUnsealedEvent(unsealService.getValue(), cardApi.getDefaultScriptType())); + Service privateKeyService = cardApi.getPrivateKeyService(messageProperty); + handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> { + EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType())); }); } catch(Exception e) { - log.error("Unseal Error: " + e.getMessage(), e); - setError("Unseal Error", e.getMessage()); - unsealButton.setDisable(false); + log.error("Private Key Error: " + e.getMessage(), e); + setError("Private Key Error", e.getMessage()); + getPrivateKeyButton.setDisable(false); + } + } + } + + private void getAddress() { + if(device.isCard()) { + try { + CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); + if(!cardApi.isInitialized()) { + if(pin.get().length() < 6) { + setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); + setContent(getCardPinEntry(getAddressButton)); + showHideLink.setVisible(false); + setExpanded(true); + getAddressButton.setDisable(false); + return; + } + + setDescription("Card not initialized"); + setContent(getCardInitializationPanel(cardApi, getAddressButton, DeviceOperation.GET_ADDRESS)); + showHideLink.setVisible(false); + setExpanded(true); + return; + } + + Service
addressService = cardApi.getAddressService(messageProperty); + handleCardOperation(addressService, getAddressButton, "Address", false, event -> { + EventManager.get().post(new DeviceAddressEvent(addressService.getValue())); + }); + } catch(Exception e) { + log.error("Address Error: " + e.getMessage(), e); + setError("Address Error", e.getMessage()); + getAddressButton.setDisable(false); } } } @@ -897,9 +961,13 @@ public class DevicePane extends TitledDescriptionPane { discoverKeystoresButton.setDefaultButton(defaultDevice); discoverKeystoresButton.setVisible(true); showHideLink.setVisible(false); - } else if(deviceOperation.equals(DeviceOperation.UNSEAL)) { - unsealButton.setDefaultButton(defaultDevice); - unsealButton.setVisible(true); + } else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) { + getPrivateKeyButton.setDefaultButton(defaultDevice); + getPrivateKeyButton.setVisible(true); + showHideLink.setVisible(false); + } else if(deviceOperation.equals(DeviceOperation.GET_ADDRESS)) { + getAddressButton.setDefaultButton(defaultDevice); + getAddressButton.setVisible(true); showHideLink.setVisible(false); } } @@ -943,12 +1011,12 @@ public class DevicePane extends TitledDescriptionPane { return contentBox; } - private Node getCardInitializationPanel(CardApi cardApi) { + private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) { 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.setPromptText("Enter input for user entropy"); entropy.setDisable(true); ToggleGroup toggleGroup = new ToggleGroup(); @@ -964,19 +1032,30 @@ public class DevicePane extends TitledDescriptionPane { Button initializeButton = new Button("Initialize"); initializeButton.setDefaultButton(true); initializeButton.setOnAction(event -> { + initializeButton.setDisable(true); byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8)); - Service cardInitializationService = cardApi.getInitializationService(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); + Service cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty); + cardInitializationService.setOnSucceeded(successEvent -> { + if(deviceOperation == DeviceOperation.IMPORT) { + AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore."); + } else if(deviceOperation == DeviceOperation.GET_ADDRESS) { + AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address."); + } + operationButton.setDisable(false); + setDefaultStatus(); + setExpanded(false); }); - 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.setOnFailed(failEvent -> { + Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException()); + if(rootCause instanceof CardAuthorizationException) { + setError(rootCause.getMessage(), null); + setContent(getCardPinEntry(operationButton)); + operationButton.setDisable(false); + } else { + log.error("Error initializing card", failEvent.getSource().getException()); + AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage()); + initializeButton.setDisable(false); + } }); cardInitializationService.start(); }); @@ -1018,6 +1097,6 @@ public class DevicePane extends TitledDescriptionPane { } public enum DeviceOperation { - IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, UNSEAL; + IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DeviceUnsealDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DeviceUnsealDialog.java index 034bfd57..becea71b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DeviceUnsealDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DeviceUnsealDialog.java @@ -4,12 +4,12 @@ import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.DeviceUnsealedEvent; +import com.sparrowwallet.sparrow.event.DeviceGetPrivateKeyEvent; import com.sparrowwallet.sparrow.io.Device; import java.util.List; -public class DeviceUnsealDialog extends DeviceDialog { +public class DeviceUnsealDialog extends DeviceDialog { public DeviceUnsealDialog(List operationFingerprints) { super(operationFingerprints); EventManager.get().register(this); @@ -20,13 +20,13 @@ public class DeviceUnsealDialog extends DeviceDialog { private void unsealPrivateKey() { DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList()); - Optional optPrivateKey = deviceUnsealDialog.showAndWait(); + Optional optPrivateKey = deviceUnsealDialog.showAndWait(); if(optPrivateKey.isPresent()) { - DeviceUnsealDialog.UnsealedKey unsealedKey = optPrivateKey.get(); - key.setText(unsealedKey.privateKey().getPrivateKeyEncoded().toBase58()); - keyScriptType.setValue(unsealedKey.scriptType()); + DeviceUnsealDialog.DevicePrivateKey devicePrivateKey = optPrivateKey.get(); + key.setText(devicePrivateKey.privateKey().getPrivateKeyEncoded().toBase58()); + keyScriptType.setValue(devicePrivateKey.scriptType()); } } @@ -305,7 +308,16 @@ public class PrivateKeySweepDialog extends Dialog { Address fromAddress = scriptType.getAddress(privateKey.getKey()); Address destAddress = getToAddress(); - ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress); + Date since = null; + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + WalletBirthDateDialog addressScanDateDialog = new WalletBirthDateDialog(null, true); + Optional optSince = addressScanDateDialog.showAndWait(); + if(optSince.isPresent()) { + since = optSince.get(); + } + } + + ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since); addressUtxosService.setOnSucceeded(successEvent -> { createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress); }); @@ -313,6 +325,12 @@ public class PrivateKeySweepDialog extends Dialog { log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException()); AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage()); }); + + if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { + ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService); + AppServices.moveToActiveWindowScreen(serviceProgressDialog); + } + addressUtxosService.start(); } catch(Exception e) { log.error("Error creating sweep transaction", e); @@ -340,8 +358,13 @@ public class PrivateKeySweepDialog extends Dialog { long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE); if(total - fee <= dustThreshold) { - AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats)."); - return; + feeRate = Transaction.DEFAULT_MIN_RELAY_FEE; + fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1; + + if(total - fee <= dustThreshold) { + AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats)."); + return; + } } Transaction transaction = new Transaction(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletBirthDateDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletBirthDateDialog.java index 2bdcaa70..3b6f2f9b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletBirthDateDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletBirthDateDialog.java @@ -21,12 +21,12 @@ import java.util.Date; public class WalletBirthDateDialog extends Dialog { private final DatePicker birthDatePicker; - public WalletBirthDateDialog(Date birthDate) { + public WalletBirthDateDialog(Date birthDate, boolean singleAddress) { final DialogPane dialogPane = getDialogPane(); AppServices.setStageIcon(dialogPane.getScene().getWindow()); - setTitle("Wallet Birth Date"); - dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:"); + setTitle(singleAddress ? "Address Scan Start Date" : "Wallet Birth Date"); + dialogPane.setHeaderText("Select an approximate date earlier than the first " + (singleAddress ? "" : "wallet") + " transaction:"); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getButtonTypes().addAll(ButtonType.CANCEL); dialogPane.setPrefWidth(420); @@ -58,7 +58,7 @@ public class WalletBirthDateDialog extends Dialog { )); }); - final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE); + final ButtonType okButtonType = new javafx.scene.control.ButtonType(singleAddress ? "Scan Address" : "Rescan Wallet", ButtonBar.ButtonData.OK_DONE); dialogPane.getButtonTypes().addAll(okButtonType); Button okButton = (Button) dialogPane.lookupButton(okButtonType); BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty()); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/DeviceAddressEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/DeviceAddressEvent.java new file mode 100644 index 00000000..da780a3c --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/DeviceAddressEvent.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.address.Address; + +public class DeviceAddressEvent { + private final Address address; + + public DeviceAddressEvent(Address address) { + this.address = address; + } + + public Address getAddress() { + return address; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/DeviceUnsealedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/DeviceGetPrivateKeyEvent.java similarity index 78% rename from src/main/java/com/sparrowwallet/sparrow/event/DeviceUnsealedEvent.java rename to src/main/java/com/sparrowwallet/sparrow/event/DeviceGetPrivateKeyEvent.java index b8507442..b27f38d8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/DeviceUnsealedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/DeviceGetPrivateKeyEvent.java @@ -3,11 +3,11 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.ScriptType; -public class DeviceUnsealedEvent { +public class DeviceGetPrivateKeyEvent { private final ECKey privateKey; private final ScriptType scriptType; - public DeviceUnsealedEvent(ECKey privateKey, ScriptType scriptType) { + public DeviceGetPrivateKeyEvent(ECKey privateKey, ScriptType scriptType) { this.privateKey = privateKey; this.scriptType = scriptType; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/CardApi.java b/src/main/java/com/sparrowwallet/sparrow/io/CardApi.java index 318f9d3a..3dfee893 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/CardApi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/CardApi.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.io; import com.google.common.base.Throwables; +import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.ScriptType; @@ -46,7 +47,7 @@ public abstract class CardApi { public abstract boolean isInitialized() throws CardException; - public abstract void initialize(byte[] entropy) throws CardException; + public abstract void initialize(int slot, byte[] entropy) throws CardException; public abstract WalletModel getCardType() throws CardException; @@ -62,7 +63,7 @@ public abstract class CardApi { public abstract Keystore getKeystore() throws CardException; - public abstract Service getInitializationService(byte[] entropy); + public abstract Service getInitializationService(byte[] entropy, StringProperty messageProperty); public abstract Service getImportService(List derivation, StringProperty messageProperty); @@ -70,7 +71,9 @@ public abstract class CardApi { public abstract Service getSignMessageService(String message, ScriptType scriptType, List derivation, StringProperty messageProperty); - public abstract Service getUnsealService(StringProperty messageProperty); + public abstract Service getPrivateKeyService(StringProperty messageProperty); + + public abstract Service
getAddressService(StringProperty messageProperty); public abstract void disconnect(); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java b/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java index 7dd8a527..7c6099c5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/CardImport.java @@ -1,8 +1,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; + void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardAuthDump.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardAuthDump.java new file mode 100644 index 00000000..a352a42c --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardAuthDump.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.io.ckcard; + +public class CardAuthDump extends CardUnseal { + boolean tampered; +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardDump.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardDump.java new file mode 100644 index 00000000..c832ca53 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardDump.java @@ -0,0 +1,26 @@ +package com.sparrowwallet.sparrow.io.ckcard; + +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.address.InvalidAddressException; + +import javax.smartcardio.CardException; + +public class CardDump extends CardResponse { + int slot; + boolean used = true; + boolean sealed; + String address; + byte[] pubkey; + + public Address getAddress() throws CardException { + try { + if(address != null) { + return Address.fromString(address); + } + } catch(InvalidAddressException e) { + throw new CardException("Invalid address provided", e); + } + + return null; + } +} 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 628d0c01..d8e0df99 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardProtocol.java @@ -88,15 +88,32 @@ public class CardProtocol { } } - public CardRead read(String cvc) throws CardException { - Map args = new HashMap<>(); - args.put("nonce", getNonce()); + public CardRead read(String cvc, int currentSlot) throws CardException { + byte[] userNonce = getNonce(); + byte[] cardNonce = lastCardNonce; - JsonObject read = sendAuth("read", args, cvc); - return gson.fromJson(read, CardRead.class); + Map args = new HashMap<>(); + args.put("nonce", userNonce); + + JsonObject read; + if(cvc == null) { + read = send("read", args); + } else { + read = sendAuth("read", args, cvc); + } + CardRead cardRead = gson.fromJson(read, CardRead.class); + + ECDSASignature ecdsaSignature = cardRead.getSignature(); + Sha256Hash verificationData = getVerificationData(cardNonce, userNonce, new byte[] { (byte)currentSlot }); + + if(!ecdsaSignature.verify(verificationData.getBytes(), cardRead.pubkey)) { + throw new CardException("Card authentication failure: Provided signature did not match public key"); + } + + return cardRead; } - public CardSetup setup(String cvc, byte[] chainCode) throws CardException { + public CardSetup setup(String cvc, int slot, byte[] chainCode) throws CardException { if(chainCode == null) { chainCode = Sha256Hash.hashTwice(secureRandom.generateSeed(128)); } @@ -106,6 +123,7 @@ public class CardProtocol { } Map args = new HashMap<>(); + args.put("slot", slot); args.put("chain_code", chainCode); JsonObject setup = sendAuth("new", args, cvc); return gson.fromJson(setup, CardSetup.class); @@ -188,16 +206,30 @@ public class CardProtocol { return gson.fromJson(backup, CardBackup.class); } - public CardUnseal unseal(String cvc) throws CardException { - CardStatus status = getStatus(); - + public CardUnseal unseal(String cvc, int slot) throws CardException { Map args = new HashMap<>(); - args.put("slot", status.getSlot()); + args.put("slot", slot); JsonObject unseal = sendAuth("unseal", args, cvc); return gson.fromJson(unseal, CardUnseal.class); } + public CardDump dump(int slot) throws CardException { + Map args = new HashMap<>(); + args.put("slot", slot); + + JsonObject dump = send("dump", args); + return gson.fromJson(dump, CardDump.class); + } + + public CardAuthDump dump(String cvc, int slot) throws CardException { + Map args = new HashMap<>(); + args.put("slot", slot); + + JsonObject dump = sendAuth("dump", args, cvc); + return gson.fromJson(dump, CardAuthDump.class); + } + public void disconnect() throws CardException { cardTransport.disconnect(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardRead.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardRead.java index d04e237d..74290a5e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardRead.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardRead.java @@ -1,7 +1,26 @@ package com.sparrowwallet.sparrow.io.ckcard; -public class CardRead { +import com.sparrowwallet.drongo.crypto.ECDSASignature; +import com.sparrowwallet.drongo.crypto.ECKey; + +import java.math.BigInteger; +import java.util.Arrays; + +public class CardRead extends CardResponse { byte[] sig; byte[] pubkey; - byte[] card_nonce; + + public ECDSASignature getSignature() { + if(sig != null) { + BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64)); + return new ECDSASignature(r, s); + } + + return null; + } + + public ECKey getPubKey() { + return ECKey.fromPublicOnly(pubkey); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java index 67ec027b..ff24c68d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardSign.java @@ -1,27 +1,5 @@ package com.sparrowwallet.sparrow.io.ckcard; -import com.sparrowwallet.drongo.crypto.ECDSASignature; -import com.sparrowwallet.drongo.crypto.ECKey; - -import java.math.BigInteger; -import java.util.Arrays; - -public class CardSign extends CardResponse { +public class CardSign extends CardRead { int slot; - byte[] sig; - byte[] pubkey; - - public ECDSASignature getSignature() { - if(sig != null) { - BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64)); - return new ECDSASignature(r, s); - } - - return null; - } - - public ECKey getPubKey() { - return ECKey.fromPublicOnly(pubkey); - } } 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 2d133696..7e471b01 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardStatus.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardStatus.java @@ -24,7 +24,7 @@ public class CardStatus extends CardResponse { boolean testnet; public boolean isInitialized() { - return getCardType() != WalletModel.TAPSIGNER || path != null; + return (getCardType() == WalletModel.TAPSIGNER && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null); } public String getIdentifier() { @@ -49,7 +49,7 @@ public class CardStatus extends CardResponse { return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD; } - public int getSlot() { + public int getCurrentSlot() { if(slots == null || slots.isEmpty()) { return 0; } @@ -57,6 +57,14 @@ public class CardStatus extends CardResponse { return slots.get(0).intValue(); } + public int getLastSlot() { + if(slots == null || slots.size() < 2) { + return 0; + } + + return slots.get(1).intValue(); + } + @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 c65c7ce1..6dcb4740 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardTransport.java @@ -66,6 +66,8 @@ public class CardTransport { cborBuilder.put(entry.getKey(), byteValue); } else if(entry.getValue() instanceof Long longValue) { cborBuilder.put(entry.getKey(), longValue); + } else if(entry.getValue() instanceof Integer integerValue) { + cborBuilder.put(entry.getKey(), integerValue); } else if(entry.getValue() instanceof Boolean booleanValue) { cborBuilder.put(entry.getKey(), booleanValue); } else if(entry.getValue() instanceof List listValue) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java index 2fb4f811..75738b03 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CkCardApi.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io.ckcard; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.*; @@ -50,9 +51,9 @@ public class CkCardApi extends CardApi { } @Override - public void initialize(byte[] chainCode) throws CardException { + public void initialize(int slot, byte[] chainCode) throws CardException { cardProtocol.verify(); - cardProtocol.setup(cvc, chainCode); + cardProtocol.setup(cvc, slot, chainCode); } @Override @@ -129,8 +130,12 @@ public class CkCardApi extends CardApi { } @Override - public Service getInitializationService(byte[] entropy) { - return new CardImportPane.CardInitializationService(new Tapsigner(), entropy); + public Service getInitializationService(byte[] entropy, StringProperty messageProperty) { + if(cardType == WalletModel.TAPSIGNER) { + return new CardImportPane.CardInitializationService(new Tapsigner(), cvc, entropy, messageProperty); + } + + return new CardInitializationService(entropy, messageProperty); } @Override @@ -245,15 +250,46 @@ public class CkCardApi extends CardApi { } @Override - public Service getUnsealService(StringProperty messageProperty) { - return new UnsealService(messageProperty); + public Service getPrivateKeyService(StringProperty messageProperty) { + return new PrivateKeyService(messageProperty); } - ECKey unseal() throws CardException { - CardUnseal cardUnseal = cardProtocol.unseal(cvc); + ECKey getPrivateKey(int slot, boolean unsealed) throws CardException { + if(unsealed) { + CardAuthDump cardAuthDump = cardProtocol.dump(cvc, slot); + return cardAuthDump.getPrivateKey(); + } + + CardUnseal cardUnseal = cardProtocol.unseal(cvc, slot); return cardUnseal.getPrivateKey(); } + @Override + public Service
getAddressService(StringProperty messageProperty) { + return new AddressService(messageProperty); + } + + Address getAddress(int currentSlot, int lastSlot, String addr) throws CardException { + if(currentSlot == lastSlot) { + CardDump cardDump = cardProtocol.dump(currentSlot); + if(!cardDump.sealed) { + return cardDump.getAddress(); + } + } + + CardRead cardRead = cardProtocol.read(null, currentSlot); + Address address = getDefaultScriptType().getAddress(cardRead.getPubKey()); + + String left = addr.substring(0, addr.indexOf('_')); + String right = addr.substring(addr.lastIndexOf('_') + 1); + + if(!address.toString().startsWith(left) || !address.toString().endsWith(right)) { + throw new CardException("Card authentication failed: Provided pubkey does not match given address"); + } + + return address; + } + @Override public void disconnect() { try { @@ -263,6 +299,37 @@ public class CkCardApi extends CardApi { } } + public class CardInitializationService extends Service { + private final byte[] chainCode; + private final StringProperty messageProperty; + + public CardInitializationService(byte[] chainCode, StringProperty messageProperty) { + this.chainCode = chainCode; + this.messageProperty = messageProperty; + } + + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Void call() throws Exception { + CardStatus cardStatus = getStatus(); + if(cardStatus.getCardType() != WalletModel.SATSCARD) { + throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + "."); + } + if(cardStatus.isInitialized()) { + throw new IllegalStateException("Card already initialized."); + } + + checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); + + initialize(cardStatus.getCurrentSlot(), chainCode); + return null; + } + }; + } + } + public class AuthDelayService extends Service { private final CardStatus cardStatus; private final IntegerProperty delayProperty; @@ -390,10 +457,10 @@ public class CkCardApi extends CardApi { } } - public class UnsealService extends Service { + public class PrivateKeyService extends Service { private final StringProperty messageProperty; - public UnsealService(StringProperty messageProperty) { + public PrivateKeyService(StringProperty messageProperty) { this.messageProperty = messageProperty; } @@ -404,12 +471,48 @@ public class CkCardApi extends CardApi { protected ECKey call() throws Exception { CardStatus cardStatus = getStatus(); if(cardStatus.getCardType() != WalletModel.SATSCARD) { - throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to unseal private keys."); + throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to retrieve a private key."); + } + + int slot = cardStatus.getCurrentSlot(); + boolean unsealed = false; + if(!cardStatus.isInitialized()) { + //If card has been unsealed, but a new slot is not initialized, retrieve private key for previous slot + slot = slot - 1; + unsealed = true; } checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); - return unseal(); + return getPrivateKey(slot, unsealed); + } + }; + } + } + + public class AddressService extends Service
{ + private final StringProperty messageProperty; + + public AddressService(StringProperty messageProperty) { + this.messageProperty = messageProperty; + } + + @Override + protected Task
createTask() { + return new Task<>() { + @Override + protected Address call() throws Exception { + CardStatus cardStatus = getStatus(); + if(cardStatus.getCardType() != WalletModel.SATSCARD) { + throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to retrieve an address."); + } + if(!cardStatus.isInitialized()) { + throw new IllegalStateException("Please re-initialize card before attempting to get the address."); + } + + checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); + + return getAddress(cardStatus.getCurrentSlot(), cardStatus.getLastSlot(), cardStatus.addr); } }; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java index 918ff41f..2f270cb8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java @@ -26,11 +26,24 @@ public class Tapsigner implements KeystoreCardImport { } @Override - public void initialize(byte[] chainCode) throws CardException { + public void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException { + if(pin.length() < 6) { + throw new CardException("PIN too short."); + } + + if(pin.length() > 32) { + throw new CardException("PIN too long."); + } + CkCardApi cardApi = null; try { - cardApi = new CkCardApi(null); - cardApi.initialize(chainCode); + cardApi = new CkCardApi(pin); + CardStatus cardStatus = cardApi.getStatus(); + if(cardStatus.isInitialized()) { + throw new IllegalStateException("Card is already initialized."); + } + cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); + cardApi.initialize(0, chainCode); } finally { if(cardApi != null) { cardApi.disconnect(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index a56a54b2..6e30fd7b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1767,15 +1767,22 @@ public class ElectrumServer { public static class AddressUtxosService extends Service> { private final Address address; + private final Date since; - public AddressUtxosService(Address address) { + public AddressUtxosService(Address address, Date since) { this.address = address; + this.since = since; } @Override protected Task> createTask() { return new Task<>() { protected List call() throws ServerException { + if(ElectrumServer.cormorant != null) { + updateProgress(-1, 0); + ElectrumServer.cormorant.checkAddressImport(address, since); + } + ElectrumServer electrumServer = new ElectrumServer(); return electrumServer.getUtxos(address); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java index 917c46fa..d02094ff 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.net.cormorant; import com.google.common.eventbus.EventBus; +import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Server; @@ -12,6 +13,8 @@ import com.sparrowwallet.sparrow.net.cormorant.electrum.ElectrumServerRunnable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Date; + public class Cormorant { private static final Logger log = LoggerFactory.getLogger(Cormorant.class); @@ -62,6 +65,11 @@ public class Cormorant { } } + public void checkAddressImport(Address address, Date since) { + //Will block until address descriptor has been added + bitcoindClient.importAddress(address, since); + } + public boolean isRunning() { return running; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java index 9e7854c3..3f9e7ff6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java @@ -153,6 +153,13 @@ public class BitcoindClient { importWallets(wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets()); } + public void importAddress(Address address, Date since) { + Map outputDescriptors = new HashMap<>(); + String addressOutputDescriptor = OutputDescriptor.toDescriptorString(address); + outputDescriptors.put(OutputDescriptor.normalize(addressOutputDescriptor), new ScanDate(since, null, false)); + importDescriptors(outputDescriptors); + } + private Map getWalletDescriptors(Collection wallets) throws ImportFailedException { List validWallets = wallets.stream().filter(Wallet::isValid).collect(Collectors.toList()); @@ -297,7 +304,7 @@ public class BitcoindClient { } if(!importingDescriptors.isEmpty()) { - log.warn("Importing descriptors " + importingDescriptors); + log.debug("Importing descriptors " + importingDescriptors); List importDescriptors = importingDescriptors.entrySet().stream() .map(entry -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index d0d54e60..22b9e1e4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -443,7 +443,7 @@ public class KeystoreController extends WalletFormController implements Initiali log.error("Error communicating with card", e); AppServices.showErrorDialog("Error communicating with card", e.getMessage()); }); - ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Authentication Delay", "Waiting for authenication delay to clear...", "/image/tapsigner.png", authDelayService); + ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Authentication Delay", "Waiting for authentication delay to clear...", "/image/tapsigner.png", authDelayService); AppServices.moveToActiveWindowScreen(serviceProgressDialog); authDelayService.start(); } else { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 5ce2ac7e..a388dde9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.CardApi; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.ExchangeSource; @@ -142,6 +143,13 @@ public class PaymentController extends WalletFormController implements Initializ } }; + private static final Wallet nfcCardWallet = new Wallet() { + @Override + public String getFullDisplayName() { + return "NFC Card..."; + } + }; + @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); @@ -162,6 +170,13 @@ public class PaymentController extends WalletFormController implements Initializ PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND, selectLinkedOnly); Optional optPayNym = payNymDialog.showAndWait(); optPayNym.ifPresent(this::setPayNym); + } else if(newValue == nfcCardWallet) { + DeviceGetAddressDialog deviceGetAddressDialog = new DeviceGetAddressDialog(Collections.emptyList()); + Optional
optAddress = deviceGetAddressDialog.showAndWait(); + if(optAddress.isPresent()) { + address.setText(optAddress.get().toString()); + label.requestFocus(); + } } else if(newValue != null) { WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE); Address freshAddress = freshNode.getAddress(); @@ -324,6 +339,10 @@ public class PaymentController extends WalletFormController implements Initializ openWalletList.add(payNymWallet); } + if(CardApi.isReaderAvailable()) { + openWalletList.add(nfcCardWallet); + } + openWallets.setItems(FXCollections.observableList(openWalletList)); } @@ -332,6 +351,10 @@ public class PaymentController extends WalletFormController implements Initializ return getPayNymGlyph(); } + if(wallet == nfcCardWallet) { + return getNfcCardGlyph(); + } + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); Storage storage = AppServices.get().getOpenWallets().get(masterWallet); if(storage != null) { @@ -639,6 +662,13 @@ public class PaymentController extends WalletFormController implements Initializ return payNymGlyph; } + public static Glyph getNfcCardGlyph() { + Glyph nfcCardGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI); + nfcCardGlyph.getStyleClass().add("nfccard-icon"); + nfcCardGlyph.setFontSize(12); + return nfcCardGlyph; + } + @Subscribe public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) { BitcoinUnit unit = sendController.getBitcoinUnit(event.getBitcoinUnit()); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java index 6d70bc0f..9141bf79 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java @@ -9,7 +9,6 @@ import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.OutputDescriptor; -import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; @@ -27,7 +26,6 @@ import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.stage.Stage; import org.controlsfx.glyphfont.Glyph; import org.fxmisc.richtext.CodeArea; import org.slf4j.Logger; @@ -207,21 +205,21 @@ public class ReceiveController extends WalletFormController implements Initializ List possibleDevices = (List)displayAddress.getUserData(); if(possibleDevices != null && !possibleDevices.isEmpty()) { if(possibleDevices.size() > 1 || possibleDevices.get(0).isNeedsPinSent() || possibleDevices.get(0).isNeedsPassphraseSent()) { - DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor); + DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor); dlg.showAndWait(); } else { Device actualDevice = possibleDevices.get(0); Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor); displayAddressService.setOnFailed(failedEvent -> { Platform.runLater(() -> { - DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor); + DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor); dlg.showAndWait(); }); }); displayAddressService.start(); } } else { - DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor); + DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor); dlg.showAndWait(); } }