mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
tapsigner and satscard initialization fixes, satscard address and private key retrieval, core address scanning support
This commit is contained in:
parent
176e440195
commit
4e3491ec64
27 changed files with 561 additions and 157 deletions
|
@ -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<Void> {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -93,7 +93,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
|||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false);
|
||||
Optional<Date> optDate = dlg.showAndWait();
|
||||
if(optDate.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
|
|
|
@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device;
|
|||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
public class DeviceDisplayAddressDialog extends DeviceDialog<String> {
|
||||
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;
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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<Keystore> 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<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()));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
|
@ -730,8 +761,8 @@ public class DevicePane extends TitledDescriptionPane {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, EventHandler<WorkerStateEvent> successHandler) {
|
||||
if(pin.get().length() < 6) {
|
||||
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> 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<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();
|
||||
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<ECKey> unsealService = cardApi.getUnsealService(messageProperty);
|
||||
handleCardOperation(unsealService, unsealButton, "Unseal", event -> {
|
||||
EventManager.get().post(new DeviceUnsealedEvent(unsealService.getValue(), cardApi.getDefaultScriptType()));
|
||||
Service<ECKey> 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<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.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<Void> 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<Void> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DeviceUnsealDialog.UnsealedKey> {
|
||||
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.DevicePrivateKey> {
|
||||
public DeviceUnsealDialog(List<String> operationFingerprints) {
|
||||
super(operationFingerprints);
|
||||
EventManager.get().register(this);
|
||||
|
@ -20,13 +20,13 @@ public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.Unsealed
|
|||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(device, defaultDevice);
|
||||
return new DevicePane(DevicePane.DeviceOperation.GET_PRIVATE_KEY, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void deviceUnsealed(DeviceUnsealedEvent event) {
|
||||
setResult(new UnsealedKey(event.getPrivateKey(), event.getScriptType()));
|
||||
public void deviceGetPrivateKey(DeviceGetPrivateKeyEvent event) {
|
||||
setResult(new DevicePrivateKey(event.getPrivateKey(), event.getScriptType()));
|
||||
}
|
||||
|
||||
public record UnsealedKey(ECKey privateKey, ScriptType scriptType) {}
|
||||
public record DevicePrivateKey(ECKey privateKey, ScriptType scriptType) {}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
|||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
|
@ -44,6 +46,7 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -290,11 +293,11 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
|||
|
||||
private void unsealPrivateKey() {
|
||||
DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList());
|
||||
Optional<DeviceUnsealDialog.UnsealedKey> optPrivateKey = deviceUnsealDialog.showAndWait();
|
||||
Optional<DeviceUnsealDialog.DevicePrivateKey> 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<Transaction> {
|
|||
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<Date> 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<Transaction> {
|
|||
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<Transaction> {
|
|||
|
||||
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();
|
||||
|
|
|
@ -21,12 +21,12 @@ import java.util.Date;
|
|||
public class WalletBirthDateDialog extends Dialog<Date> {
|
||||
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<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);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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<Void> getInitializationService(byte[] entropy);
|
||||
public abstract Service<Void> getInitializationService(byte[] entropy, 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<ECKey> getUnsealService(StringProperty messageProperty);
|
||||
public abstract Service<ECKey> getPrivateKeyService(StringProperty messageProperty);
|
||||
|
||||
public abstract Service<Address> getAddressService(StringProperty messageProperty);
|
||||
|
||||
public abstract void disconnect();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardAuthDump extends CardUnseal {
|
||||
boolean tampered;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -88,15 +88,32 @@ public class CardProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
public CardRead read(String cvc) throws CardException {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<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 {
|
||||
cardTransport.disconnect();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{" +
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Void> getInitializationService(byte[] entropy) {
|
||||
return new CardImportPane.CardInitializationService(new Tapsigner(), entropy);
|
||||
public Service<Void> 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<ECKey> getUnsealService(StringProperty messageProperty) {
|
||||
return new UnsealService(messageProperty);
|
||||
public Service<ECKey> 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<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
|
||||
public void disconnect() {
|
||||
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> {
|
||||
private final CardStatus cardStatus;
|
||||
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;
|
||||
|
||||
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<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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1767,15 +1767,22 @@ public class ElectrumServer {
|
|||
|
||||
public static class AddressUtxosService extends Service<List<TransactionOutput>> {
|
||||
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<List<TransactionOutput>> createTask() {
|
||||
return new Task<>() {
|
||||
protected List<TransactionOutput> call() throws ServerException {
|
||||
if(ElectrumServer.cormorant != null) {
|
||||
updateProgress(-1, 0);
|
||||
ElectrumServer.cormorant.checkAddressImport(address, since);
|
||||
}
|
||||
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.getUtxos(address);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -153,6 +153,13 @@ public class BitcoindClient {
|
|||
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 {
|
||||
List<Wallet> 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<ImportDescriptor> importDescriptors = importingDescriptors.entrySet().stream()
|
||||
.map(entry -> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<PayNym> optPayNym = payNymDialog.showAndWait();
|
||||
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) {
|
||||
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());
|
||||
|
|
|
@ -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<Device> possibleDevices = (List<Device>)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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue