tapsigner and satscard initialization fixes, satscard address and private key retrieval, core address scanning support

This commit is contained in:
Craig Raw 2023-02-01 09:39:49 +02:00
parent 176e440195
commit 4e3491ec64
27 changed files with 561 additions and 157 deletions

View file

@ -55,30 +55,19 @@ public class CardImportPane extends TitledDescriptionPane {
importButton.setGraphic(tapGlyph); importButton.setGraphic(tapGlyph);
importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> { importButton.setOnAction(event -> {
importButton.setDisable(true);
importCard(); importCard();
}); });
return importButton; return importButton;
} }
private void importCard() { 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) { if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getPinEntry()); setContent(getPinEntry());
showHideLink.setVisible(false); showHideLink.setVisible(false);
setExpanded(true); setExpanded(true);
importButton.setDisable(false);
return; return;
} }
@ -86,6 +75,21 @@ public class CardImportPane extends TitledDescriptionPane {
messageProperty.addListener((observable, oldValue, newValue) -> { messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(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 cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
cardImportService.setOnSucceeded(event -> { cardImportService.setOnSucceeded(event -> {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue())); 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()); log.error("Error importing keystore from card", event.getSource().getException());
setError("Import Error", rootCause.getMessage()); setError("Import Error", rootCause.getMessage());
} }
importButton.setDisable(false);
}); });
cardImportService.start(); cardImportService.start();
} }
private Node getInitializationPanel() { private Node getInitializationPanel(StringProperty messageProperty) {
VBox initTypeBox = new VBox(5); VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)"); RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced"); RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField(); TextField entropy = new TextField();
entropy.setPromptText("Enter input for chain code"); entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true); entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup(); ToggleGroup toggleGroup = new ToggleGroup();
@ -124,18 +129,26 @@ public class CardImportPane extends TitledDescriptionPane {
Button initializeButton = new Button("Initialize"); Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true); initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> { initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8)); byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
CardInitializationService cardInitializationService = new CardInitializationService(importer, chainCode); CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), chainCode, messageProperty);
cardInitializationService.setOnSucceeded(event1 -> { cardInitializationService.setOnSucceeded(successEvent -> {
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."); AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
setDescription("Enter PIN code"); setDescription("Leave card on reader");
setContent(getPinEntry()); setExpanded(false);
setExpanded(true); importButton.setDisable(false);
}); });
cardInitializationService.setOnFailed(event1 -> { cardInitializationService.setOnFailed(failEvent -> {
Throwable e = event1.getSource().getException(); Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
log.error("Error initializing card", e); if(rootCause instanceof CardAuthorizationException) {
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + e.getMessage()); 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(); cardInitializationService.start();
}); });
@ -172,11 +185,15 @@ public class CardImportPane extends TitledDescriptionPane {
public static class CardInitializationService extends Service<Void> { public static class CardInitializationService extends Service<Void> {
private final KeystoreCardImport cardImport; private final KeystoreCardImport cardImport;
private final String pin;
private final byte[] chainCode; 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.cardImport = cardImport;
this.pin = pin;
this.chainCode = chainCode; this.chainCode = chainCode;
this.messageProperty = messageProperty;
} }
@Override @Override
@ -184,7 +201,7 @@ public class CardImportPane extends TitledDescriptionPane {
return new Task<>() { return new Task<>() {
@Override @Override
protected Void call() throws Exception { protected Void call() throws Exception {
cardImport.initialize(chainCode); cardImport.initialize(pin, chainCode, messageProperty);
return null; return null;
} }
}; };

View file

@ -93,7 +93,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
Hyperlink hyperlink = new Hyperlink(); Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30); hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> { hyperlink.setOnAction(event -> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate()); WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false);
Optional<Date> optDate = dlg.showAndWait(); Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) { if(optDate.isPresent()) {
Storage storage = AppServices.get().getOpenWallets().get(wallet); Storage storage = AppServices.get().getOpenWallets().get(wallet);

View file

@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class DeviceAddressDialog extends DeviceDialog<String> { public class DeviceDisplayAddressDialog extends DeviceDialog<String> {
private final Wallet wallet; private final Wallet wallet;
private final OutputDescriptor outputDescriptor; 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())); super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
this.wallet = wallet; this.wallet = wallet;
this.outputDescriptor = outputDescriptor; this.outputDescriptor = outputDescriptor;

View file

@ -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<Address> {
public DeviceGetAddressDialog(List<String> 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());
}
}

View file

@ -4,6 +4,7 @@ import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.Policy;
@ -66,7 +67,8 @@ public class DevicePane extends TitledDescriptionPane {
private Button displayAddressButton; private Button displayAddressButton;
private Button signMessageButton; private Button signMessageButton;
private Button discoverKeystoresButton; private Button discoverKeystoresButton;
private Button unsealButton; private Button getPrivateKeyButton;
private Button getAddressButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty(""); private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private final SimpleStringProperty pin = new SimpleStringProperty(""); private final SimpleStringProperty pin = new SimpleStringProperty("");
@ -201,9 +203,9 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton); 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"); super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.UNSEAL; this.deviceOperation = deviceOperation;
this.wallet = null; this.wallet = null;
this.psbt = null; this.psbt = null;
this.outputDescriptor = null; this.outputDescriptor = null;
@ -216,7 +218,16 @@ public class DevicePane extends TitledDescriptionPane {
setDefaultStatus(); setDefaultStatus();
showHideLink.setVisible(false); 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); initialise(device);
@ -224,7 +235,7 @@ public class DevicePane extends TitledDescriptionPane {
Platform.runLater(() -> setDescription(newValue)); Platform.runLater(() -> setDescription(newValue));
}); });
buttonBox.getChildren().add(unsealButton); buttonBox.getChildren().add(button);
} }
private void initialise(Device device) { private void initialise(Device device) {
@ -250,7 +261,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
private void setDefaultStatus() { 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() { private void createUnlockButton() {
@ -370,15 +381,26 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setVisible(false); discoverKeystoresButton.setVisible(false);
} }
private void createUnsealButton() { private void createGetPrivateKeyButton() {
unsealButton = new Button("Unseal"); getPrivateKeyButton = new Button("Get Private Key");
unsealButton.setAlignment(Pos.CENTER_RIGHT); getPrivateKeyButton.setAlignment(Pos.CENTER_RIGHT);
unsealButton.setOnAction(event -> { getPrivateKeyButton.setOnAction(event -> {
unsealButton.setDisable(true); getPrivateKeyButton.setDisable(true);
unseal(); getPrivateKey();
}); });
unsealButton.managedProperty().bind(unsealButton.visibleProperty()); getPrivateKeyButton.managedProperty().bind(getPrivateKeyButton.visibleProperty());
unsealButton.setVisible(false); 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) { private void unlock(Device device) {
@ -618,15 +640,24 @@ public class DevicePane extends TitledDescriptionPane {
try { try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
if(!cardApi.isInitialized()) { 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"); setDescription("Card not initialized");
setContent(getCardInitializationPanel(cardApi)); setContent(getCardInitializationPanel(cardApi, importButton, DeviceOperation.IMPORT));
showHideLink.setVisible(false); showHideLink.setVisible(false);
setExpanded(true); setExpanded(true);
return; return;
} }
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty); Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", event -> { handleCardOperation(importService, importButton, "Import", true, event -> {
importKeystore(derivation, importService.getValue()); importKeystore(derivation, importService.getValue());
}); });
} catch(Exception e) { } catch(Exception e) {
@ -705,7 +736,7 @@ public class DevicePane extends TitledDescriptionPane {
try { try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty); Service<PSBT> 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())); EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
}); });
} catch(Exception e) { } catch(Exception e) {
@ -730,8 +761,8 @@ public class DevicePane extends TitledDescriptionPane {
} }
} }
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, EventHandler<WorkerStateEvent> successHandler) { private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
if(pin.get().length() < 6) { if(pinRequired && pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short"); setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(operationButton)); setContent(getCardPinEntry(operationButton));
showHideLink.setVisible(false); showHideLink.setVisible(false);
@ -774,7 +805,7 @@ public class DevicePane extends TitledDescriptionPane {
try { try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty); Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
handleCardOperation(signMessageService, signMessageButton, "Signing", event -> { handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
String signature = signMessageService.getValue(); String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature)); EventManager.get().post(new MessageSignedEvent(wallet, signature));
}); });
@ -855,18 +886,51 @@ public class DevicePane extends TitledDescriptionPane {
getXpubsService.start(); getXpubsService.start();
} }
private void unseal() { private void getPrivateKey() {
if(device.isCard()) { if(device.isCard()) {
try { try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get()); CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<ECKey> unsealService = cardApi.getUnsealService(messageProperty); Service<ECKey> privateKeyService = cardApi.getPrivateKeyService(messageProperty);
handleCardOperation(unsealService, unsealButton, "Unseal", event -> { handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> {
EventManager.get().post(new DeviceUnsealedEvent(unsealService.getValue(), cardApi.getDefaultScriptType())); EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType()));
}); });
} catch(Exception e) { } catch(Exception e) {
log.error("Unseal Error: " + e.getMessage(), e); log.error("Private Key Error: " + e.getMessage(), e);
setError("Unseal Error", e.getMessage()); setError("Private Key Error", e.getMessage());
unsealButton.setDisable(false); 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<Address> 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.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true); discoverKeystoresButton.setVisible(true);
showHideLink.setVisible(false); showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.UNSEAL)) { } else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
unsealButton.setDefaultButton(defaultDevice); getPrivateKeyButton.setDefaultButton(defaultDevice);
unsealButton.setVisible(true); getPrivateKeyButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.GET_ADDRESS)) {
getAddressButton.setDefaultButton(defaultDevice);
getAddressButton.setVisible(true);
showHideLink.setVisible(false); showHideLink.setVisible(false);
} }
} }
@ -943,12 +1011,12 @@ public class DevicePane extends TitledDescriptionPane {
return contentBox; return contentBox;
} }
private Node getCardInitializationPanel(CardApi cardApi) { private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
VBox initTypeBox = new VBox(5); VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)"); RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced"); RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField(); TextField entropy = new TextField();
entropy.setPromptText("Enter input for chain code"); entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true); entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup(); ToggleGroup toggleGroup = new ToggleGroup();
@ -964,19 +1032,30 @@ public class DevicePane extends TitledDescriptionPane {
Button initializeButton = new Button("Initialize"); Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true); initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> { initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8)); byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode); Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
cardInitializationService.setOnSucceeded(event1 -> { cardInitializationService.setOnSucceeded(successEvent -> {
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."); if(deviceOperation == DeviceOperation.IMPORT) {
setDescription("Enter PIN code"); AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
setContent(getCardPinEntry(importButton)); } else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
importButton.setDisable(false); AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
setExpanded(true); }
operationButton.setDisable(false);
setDefaultStatus();
setExpanded(false);
}); });
cardInitializationService.setOnFailed(event1 -> { cardInitializationService.setOnFailed(failEvent -> {
Throwable e = event1.getSource().getException(); Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
log.error("Error initializing card", e); if(rootCause instanceof CardAuthorizationException) {
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + e.getMessage()); 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(); cardInitializationService.start();
}); });
@ -1018,6 +1097,6 @@ public class DevicePane extends TitledDescriptionPane {
} }
public enum DeviceOperation { 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;
} }
} }

View file

@ -4,12 +4,12 @@ import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.DeviceUnsealedEvent; import com.sparrowwallet.sparrow.event.DeviceGetPrivateKeyEvent;
import com.sparrowwallet.sparrow.io.Device; import com.sparrowwallet.sparrow.io.Device;
import java.util.List; import java.util.List;
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.UnsealedKey> { public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.DevicePrivateKey> {
public DeviceUnsealDialog(List<String> operationFingerprints) { public DeviceUnsealDialog(List<String> operationFingerprints) {
super(operationFingerprints); super(operationFingerprints);
EventManager.get().register(this); EventManager.get().register(this);
@ -20,13 +20,13 @@ public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.Unsealed
@Override @Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) { protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(device, defaultDevice); return new DevicePane(DevicePane.DeviceOperation.GET_PRIVATE_KEY, device, defaultDevice);
} }
@Subscribe @Subscribe
public void deviceUnsealed(DeviceUnsealedEvent event) { public void deviceGetPrivateKey(DeviceGetPrivateKeyEvent event) {
setResult(new UnsealedKey(event.getPrivateKey(), event.getScriptType())); setResult(new DevicePrivateKey(event.getPrivateKey(), event.getScriptType()));
} }
public record UnsealedKey(ECKey privateKey, ScriptType scriptType) {} public record DevicePrivateKey(ECKey privateKey, ScriptType scriptType) {}
} }

View file

@ -15,7 +15,9 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi; import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
@ -44,6 +46,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -290,11 +293,11 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private void unsealPrivateKey() { private void unsealPrivateKey() {
DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList()); DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList());
Optional<DeviceUnsealDialog.UnsealedKey> optPrivateKey = deviceUnsealDialog.showAndWait(); Optional<DeviceUnsealDialog.DevicePrivateKey> optPrivateKey = deviceUnsealDialog.showAndWait();
if(optPrivateKey.isPresent()) { if(optPrivateKey.isPresent()) {
DeviceUnsealDialog.UnsealedKey unsealedKey = optPrivateKey.get(); DeviceUnsealDialog.DevicePrivateKey devicePrivateKey = optPrivateKey.get();
key.setText(unsealedKey.privateKey().getPrivateKeyEncoded().toBase58()); key.setText(devicePrivateKey.privateKey().getPrivateKeyEncoded().toBase58());
keyScriptType.setValue(unsealedKey.scriptType()); keyScriptType.setValue(devicePrivateKey.scriptType());
} }
} }
@ -305,7 +308,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
Address fromAddress = scriptType.getAddress(privateKey.getKey()); Address fromAddress = scriptType.getAddress(privateKey.getKey());
Address destAddress = getToAddress(); 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<Date> optSince = addressScanDateDialog.showAndWait();
if(optSince.isPresent()) {
since = optSince.get();
}
}
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
addressUtxosService.setOnSucceeded(successEvent -> { addressUtxosService.setOnSucceeded(successEvent -> {
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress); createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
}); });
@ -313,6 +325,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException()); log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage()); 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(); addressUtxosService.start();
} catch(Exception e) { } catch(Exception e) {
log.error("Error creating sweep transaction", e); log.error("Error creating sweep transaction", e);
@ -340,8 +358,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE); long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
if(total - fee <= dustThreshold) { if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats)."); feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
return; 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(); Transaction transaction = new Transaction();

View file

@ -21,12 +21,12 @@ import java.util.Date;
public class WalletBirthDateDialog extends Dialog<Date> { public class WalletBirthDateDialog extends Dialog<Date> {
private final DatePicker birthDatePicker; private final DatePicker birthDatePicker;
public WalletBirthDateDialog(Date birthDate) { public WalletBirthDateDialog(Date birthDate, boolean singleAddress) {
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Wallet Birth Date"); setTitle(singleAddress ? "Address Scan Start Date" : "Wallet Birth Date");
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:"); dialogPane.setHeaderText("Select an approximate date earlier than the first " + (singleAddress ? "" : "wallet") + " transaction:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL); dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(420); dialogPane.setPrefWidth(420);
@ -58,7 +58,7 @@ public class WalletBirthDateDialog extends Dialog<Date> {
)); ));
}); });
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); dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType); Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty()); BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());

View file

@ -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;
}
}

View file

@ -3,11 +3,11 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
public class DeviceUnsealedEvent { public class DeviceGetPrivateKeyEvent {
private final ECKey privateKey; private final ECKey privateKey;
private final ScriptType scriptType; private final ScriptType scriptType;
public DeviceUnsealedEvent(ECKey privateKey, ScriptType scriptType) { public DeviceGetPrivateKeyEvent(ECKey privateKey, ScriptType scriptType) {
this.privateKey = privateKey; this.privateKey = privateKey;
this.scriptType = scriptType; this.scriptType = scriptType;
} }

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io; package com.sparrowwallet.sparrow.io;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
@ -46,7 +47,7 @@ public abstract class CardApi {
public abstract boolean isInitialized() throws CardException; 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; public abstract WalletModel getCardType() throws CardException;
@ -62,7 +63,7 @@ public abstract class CardApi {
public abstract Keystore getKeystore() throws CardException; public abstract Keystore getKeystore() throws CardException;
public abstract Service<Void> getInitializationService(byte[] entropy); public abstract Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty);
public abstract Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty); public abstract Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty);
@ -70,7 +71,9 @@ public abstract class CardApi {
public abstract Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty); public abstract Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty);
public abstract Service<ECKey> getUnsealService(StringProperty messageProperty); public abstract Service<ECKey> getPrivateKeyService(StringProperty messageProperty);
public abstract Service<Address> getAddressService(StringProperty messageProperty);
public abstract void disconnect(); public abstract void disconnect();

View file

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

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.io.ckcard;
public class CardAuthDump extends CardUnseal {
boolean tampered;
}

View file

@ -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;
}
}

View file

@ -88,15 +88,32 @@ public class CardProtocol {
} }
} }
public CardRead read(String cvc) throws CardException { public CardRead read(String cvc, int currentSlot) throws CardException {
Map<String, Object> args = new HashMap<>(); byte[] userNonce = getNonce();
args.put("nonce", getNonce()); byte[] cardNonce = lastCardNonce;
JsonObject read = sendAuth("read", args, cvc); Map<String, Object> args = new HashMap<>();
return gson.fromJson(read, CardRead.class); 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) { if(chainCode == null) {
chainCode = Sha256Hash.hashTwice(secureRandom.generateSeed(128)); chainCode = Sha256Hash.hashTwice(secureRandom.generateSeed(128));
} }
@ -106,6 +123,7 @@ public class CardProtocol {
} }
Map<String, Object> args = new HashMap<>(); Map<String, Object> args = new HashMap<>();
args.put("slot", slot);
args.put("chain_code", chainCode); args.put("chain_code", chainCode);
JsonObject setup = sendAuth("new", args, cvc); JsonObject setup = sendAuth("new", args, cvc);
return gson.fromJson(setup, CardSetup.class); return gson.fromJson(setup, CardSetup.class);
@ -188,16 +206,30 @@ public class CardProtocol {
return gson.fromJson(backup, CardBackup.class); return gson.fromJson(backup, CardBackup.class);
} }
public CardUnseal unseal(String cvc) throws CardException { public CardUnseal unseal(String cvc, int slot) throws CardException {
CardStatus status = getStatus();
Map<String, Object> args = new HashMap<>(); Map<String, Object> args = new HashMap<>();
args.put("slot", status.getSlot()); args.put("slot", slot);
JsonObject unseal = sendAuth("unseal", args, cvc); JsonObject unseal = sendAuth("unseal", args, cvc);
return gson.fromJson(unseal, CardUnseal.class); return gson.fromJson(unseal, CardUnseal.class);
} }
public CardDump dump(int slot) throws CardException {
Map<String, Object> 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<String, Object> args = new HashMap<>();
args.put("slot", slot);
JsonObject dump = sendAuth("dump", args, cvc);
return gson.fromJson(dump, CardAuthDump.class);
}
public void disconnect() throws CardException { public void disconnect() throws CardException {
cardTransport.disconnect(); cardTransport.disconnect();
} }

View file

@ -1,7 +1,26 @@
package com.sparrowwallet.sparrow.io.ckcard; 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[] sig;
byte[] pubkey; 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);
}
} }

View file

@ -1,27 +1,5 @@
package com.sparrowwallet.sparrow.io.ckcard; package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.crypto.ECDSASignature; public class CardSign extends CardRead {
import com.sparrowwallet.drongo.crypto.ECKey;
import java.math.BigInteger;
import java.util.Arrays;
public class CardSign extends CardResponse {
int slot; 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);
}
} }

View file

@ -24,7 +24,7 @@ public class CardStatus extends CardResponse {
boolean testnet; boolean testnet;
public boolean isInitialized() { public boolean isInitialized() {
return getCardType() != WalletModel.TAPSIGNER || path != null; return (getCardType() == WalletModel.TAPSIGNER && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null);
} }
public String getIdentifier() { public String getIdentifier() {
@ -49,7 +49,7 @@ public class CardStatus extends CardResponse {
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD; return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
} }
public int getSlot() { public int getCurrentSlot() {
if(slots == null || slots.isEmpty()) { if(slots == null || slots.isEmpty()) {
return 0; return 0;
} }
@ -57,6 +57,14 @@ public class CardStatus extends CardResponse {
return slots.get(0).intValue(); return slots.get(0).intValue();
} }
public int getLastSlot() {
if(slots == null || slots.size() < 2) {
return 0;
}
return slots.get(1).intValue();
}
@Override @Override
public String toString() { public String toString() {
return "CardStatus{" + return "CardStatus{" +

View file

@ -66,6 +66,8 @@ public class CardTransport {
cborBuilder.put(entry.getKey(), byteValue); cborBuilder.put(entry.getKey(), byteValue);
} else if(entry.getValue() instanceof Long longValue) { } else if(entry.getValue() instanceof Long longValue) {
cborBuilder.put(entry.getKey(), 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) { } else if(entry.getValue() instanceof Boolean booleanValue) {
cborBuilder.put(entry.getKey(), booleanValue); cborBuilder.put(entry.getKey(), booleanValue);
} else if(entry.getValue() instanceof List<?> listValue) { } else if(entry.getValue() instanceof List<?> listValue) {

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
@ -50,9 +51,9 @@ public class CkCardApi extends CardApi {
} }
@Override @Override
public void initialize(byte[] chainCode) throws CardException { public void initialize(int slot, byte[] chainCode) throws CardException {
cardProtocol.verify(); cardProtocol.verify();
cardProtocol.setup(cvc, chainCode); cardProtocol.setup(cvc, slot, chainCode);
} }
@Override @Override
@ -129,8 +130,12 @@ public class CkCardApi extends CardApi {
} }
@Override @Override
public Service<Void> getInitializationService(byte[] entropy) { public Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty) {
return new CardImportPane.CardInitializationService(new Tapsigner(), entropy); if(cardType == WalletModel.TAPSIGNER) {
return new CardImportPane.CardInitializationService(new Tapsigner(), cvc, entropy, messageProperty);
}
return new CardInitializationService(entropy, messageProperty);
} }
@Override @Override
@ -245,15 +250,46 @@ public class CkCardApi extends CardApi {
} }
@Override @Override
public Service<ECKey> getUnsealService(StringProperty messageProperty) { public Service<ECKey> getPrivateKeyService(StringProperty messageProperty) {
return new UnsealService(messageProperty); return new PrivateKeyService(messageProperty);
} }
ECKey unseal() throws CardException { ECKey getPrivateKey(int slot, boolean unsealed) throws CardException {
CardUnseal cardUnseal = cardProtocol.unseal(cvc); if(unsealed) {
CardAuthDump cardAuthDump = cardProtocol.dump(cvc, slot);
return cardAuthDump.getPrivateKey();
}
CardUnseal cardUnseal = cardProtocol.unseal(cvc, slot);
return cardUnseal.getPrivateKey(); return cardUnseal.getPrivateKey();
} }
@Override
public Service<Address> 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 @Override
public void disconnect() { public void disconnect() {
try { try {
@ -263,6 +299,37 @@ public class CkCardApi extends CardApi {
} }
} }
public class CardInitializationService extends Service<Void> {
private final byte[] chainCode;
private final StringProperty messageProperty;
public CardInitializationService(byte[] chainCode, StringProperty messageProperty) {
this.chainCode = chainCode;
this.messageProperty = messageProperty;
}
@Override
protected Task<Void> 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<Void> { public class AuthDelayService extends Service<Void> {
private final CardStatus cardStatus; private final CardStatus cardStatus;
private final IntegerProperty delayProperty; private final IntegerProperty delayProperty;
@ -390,10 +457,10 @@ public class CkCardApi extends CardApi {
} }
} }
public class UnsealService extends Service<ECKey> { public class PrivateKeyService extends Service<ECKey> {
private final StringProperty messageProperty; private final StringProperty messageProperty;
public UnsealService(StringProperty messageProperty) { public PrivateKeyService(StringProperty messageProperty) {
this.messageProperty = messageProperty; this.messageProperty = messageProperty;
} }
@ -404,12 +471,48 @@ public class CkCardApi extends CardApi {
protected ECKey call() throws Exception { protected ECKey call() throws Exception {
CardStatus cardStatus = getStatus(); CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.SATSCARD) { 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); checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
return unseal(); return getPrivateKey(slot, unsealed);
}
};
}
}
public class AddressService extends Service<Address> {
private final StringProperty messageProperty;
public AddressService(StringProperty messageProperty) {
this.messageProperty = messageProperty;
}
@Override
protected Task<Address> 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);
} }
}; };
} }

View file

@ -26,11 +26,24 @@ public class Tapsigner implements KeystoreCardImport {
} }
@Override @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; CkCardApi cardApi = null;
try { try {
cardApi = new CkCardApi(null); cardApi = new CkCardApi(pin);
cardApi.initialize(chainCode); 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 { } finally {
if(cardApi != null) { if(cardApi != null) {
cardApi.disconnect(); cardApi.disconnect();

View file

@ -1767,15 +1767,22 @@ public class ElectrumServer {
public static class AddressUtxosService extends Service<List<TransactionOutput>> { public static class AddressUtxosService extends Service<List<TransactionOutput>> {
private final Address address; private final Address address;
private final Date since;
public AddressUtxosService(Address address) { public AddressUtxosService(Address address, Date since) {
this.address = address; this.address = address;
this.since = since;
} }
@Override @Override
protected Task<List<TransactionOutput>> createTask() { protected Task<List<TransactionOutput>> createTask() {
return new Task<>() { return new Task<>() {
protected List<TransactionOutput> call() throws ServerException { protected List<TransactionOutput> call() throws ServerException {
if(ElectrumServer.cormorant != null) {
updateProgress(-1, 0);
ElectrumServer.cormorant.checkAddressImport(address, since);
}
ElectrumServer electrumServer = new ElectrumServer(); ElectrumServer electrumServer = new ElectrumServer();
return electrumServer.getUtxos(address); return electrumServer.getUtxos(address);
} }

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.net.cormorant; package com.sparrowwallet.sparrow.net.cormorant;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Server; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Date;
public class Cormorant { public class Cormorant {
private static final Logger log = LoggerFactory.getLogger(Cormorant.class); 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() { public boolean isRunning() {
return running; return running;
} }

View file

@ -153,6 +153,13 @@ public class BitcoindClient {
importWallets(wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets()); importWallets(wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets());
} }
public void importAddress(Address address, Date since) {
Map<String, ScanDate> outputDescriptors = new HashMap<>();
String addressOutputDescriptor = OutputDescriptor.toDescriptorString(address);
outputDescriptors.put(OutputDescriptor.normalize(addressOutputDescriptor), new ScanDate(since, null, false));
importDescriptors(outputDescriptors);
}
private Map<String, ScanDate> getWalletDescriptors(Collection<Wallet> wallets) throws ImportFailedException { private Map<String, ScanDate> getWalletDescriptors(Collection<Wallet> wallets) throws ImportFailedException {
List<Wallet> validWallets = wallets.stream().filter(Wallet::isValid).collect(Collectors.toList()); List<Wallet> validWallets = wallets.stream().filter(Wallet::isValid).collect(Collectors.toList());
@ -297,7 +304,7 @@ public class BitcoindClient {
} }
if(!importingDescriptors.isEmpty()) { if(!importingDescriptors.isEmpty()) {
log.warn("Importing descriptors " + importingDescriptors); log.debug("Importing descriptors " + importingDescriptors);
List<ImportDescriptor> importDescriptors = importingDescriptors.entrySet().stream() List<ImportDescriptor> importDescriptors = importingDescriptors.entrySet().stream()
.map(entry -> { .map(entry -> {

View file

@ -443,7 +443,7 @@ public class KeystoreController extends WalletFormController implements Initiali
log.error("Error communicating with card", e); log.error("Error communicating with card", e);
AppServices.showErrorDialog("Error communicating with card", e.getMessage()); 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); AppServices.moveToActiveWindowScreen(serviceProgressDialog);
authDelayService.start(); authDelayService.start();
} else { } else {

View file

@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ExchangeSource; 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 @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this); 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); PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND, selectLinkedOnly);
Optional<PayNym> optPayNym = payNymDialog.showAndWait(); Optional<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(this::setPayNym); optPayNym.ifPresent(this::setPayNym);
} else if(newValue == nfcCardWallet) {
DeviceGetAddressDialog deviceGetAddressDialog = new DeviceGetAddressDialog(Collections.emptyList());
Optional<Address> optAddress = deviceGetAddressDialog.showAndWait();
if(optAddress.isPresent()) {
address.setText(optAddress.get().toString());
label.requestFocus();
}
} else if(newValue != null) { } else if(newValue != null) {
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE); WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = freshNode.getAddress(); Address freshAddress = freshNode.getAddress();
@ -324,6 +339,10 @@ public class PaymentController extends WalletFormController implements Initializ
openWalletList.add(payNymWallet); openWalletList.add(payNymWallet);
} }
if(CardApi.isReaderAvailable()) {
openWalletList.add(nfcCardWallet);
}
openWallets.setItems(FXCollections.observableList(openWalletList)); openWallets.setItems(FXCollections.observableList(openWalletList));
} }
@ -332,6 +351,10 @@ public class PaymentController extends WalletFormController implements Initializ
return getPayNymGlyph(); return getPayNymGlyph();
} }
if(wallet == nfcCardWallet) {
return getNfcCardGlyph();
}
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
Storage storage = AppServices.get().getOpenWallets().get(masterWallet); Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
if(storage != null) { if(storage != null) {
@ -639,6 +662,13 @@ public class PaymentController extends WalletFormController implements Initializ
return payNymGlyph; 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 @Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) { public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
BitcoinUnit unit = sendController.getBitcoinUnit(event.getBitcoinUnit()); BitcoinUnit unit = sendController.getBitcoinUnit(event.getBitcoinUnit());

View file

@ -9,7 +9,6 @@ import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
@ -27,7 +26,6 @@ import javafx.fxml.Initializable;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.CodeArea;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -207,21 +205,21 @@ public class ReceiveController extends WalletFormController implements Initializ
List<Device> possibleDevices = (List<Device>)displayAddress.getUserData(); List<Device> possibleDevices = (List<Device>)displayAddress.getUserData();
if(possibleDevices != null && !possibleDevices.isEmpty()) { if(possibleDevices != null && !possibleDevices.isEmpty()) {
if(possibleDevices.size() > 1 || possibleDevices.get(0).isNeedsPinSent() || possibleDevices.get(0).isNeedsPassphraseSent()) { 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(); dlg.showAndWait();
} else { } else {
Device actualDevice = possibleDevices.get(0); Device actualDevice = possibleDevices.get(0);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor); Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor);
displayAddressService.setOnFailed(failedEvent -> { displayAddressService.setOnFailed(failedEvent -> {
Platform.runLater(() -> { Platform.runLater(() -> {
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor); DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);
dlg.showAndWait(); dlg.showAndWait();
}); });
}); });
displayAddressService.start(); displayAddressService.start();
} }
} else { } else {
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor); DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);
dlg.showAndWait(); dlg.showAndWait();
} }
} }