mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
add functionality for tapsigner backup and pin change
This commit is contained in:
parent
6b59ff60ad
commit
3ddf4ed4b2
8 changed files with 283 additions and 21 deletions
|
@ -0,0 +1,93 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.scene.control.*;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||
private final CustomPasswordField existingPin;
|
||||
private final CustomPasswordField newPin;
|
||||
private final CustomPasswordField newPinConfirm;
|
||||
private final CheckBox backupFirst;
|
||||
private final ButtonType okButtonType;
|
||||
|
||||
public CardPinDialog() {
|
||||
this.existingPin = new ViewPasswordField();
|
||||
this.newPin = new ViewPasswordField();
|
||||
this.newPinConfirm = new ViewPasswordField();
|
||||
this.backupFirst = new CheckBox();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Change Card PIN");
|
||||
dialogPane.setHeaderText("Enter the current PIN, and then the new PIN twice. PIN must be between 6 and 32 digits.");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(260);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
|
||||
lock.setFontSize(50);
|
||||
dialogPane.setGraphic(lock);
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field currentField = new Field();
|
||||
currentField.setText("Current PIN:");
|
||||
currentField.getInputs().add(existingPin);
|
||||
|
||||
Field newField = new Field();
|
||||
newField.setText("New PIN:");
|
||||
newField.getInputs().add(newPin);
|
||||
|
||||
Field confirmField = new Field();
|
||||
confirmField.setText("Confirm new PIN:");
|
||||
confirmField.getInputs().add(newPinConfirm);
|
||||
|
||||
Field backupField = new Field();
|
||||
backupField.setText("Backup First:");
|
||||
backupField.getInputs().add(backupFirst);
|
||||
|
||||
fieldset.getChildren().addAll(currentField, newField, confirmField, backupField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater( () -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(existingPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", existingPin.getText().length() < 6 || existingPin.getText().length() > 32));
|
||||
validationSupport.registerValidator(newPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", newPin.getText().length() < 6 || newPin.getText().length() > 32));
|
||||
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
|
||||
});
|
||||
|
||||
okButtonType = new javafx.scene.control.ButtonType("Change", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
okButton.setPrefWidth(130);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> existingPin.getText().length() < 6 || existingPin.getText().length() > 32
|
||||
|| newPin.getText().length() < 6 || newPin.getText().length() > 32
|
||||
|| !newPin.getText().equals(newPinConfirm.getText()),
|
||||
existingPin.textProperty(), newPin.textProperty(), newPinConfirm.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
Platform.runLater(existingPin::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? new CardPinChange(existingPin.getText(), newPin.getText(), backupFirst.isSelected()) : null);
|
||||
}
|
||||
|
||||
public record CardPinChange(String currentPin, String newPin, boolean backupFirst) { }
|
||||
}
|
|
@ -8,7 +8,12 @@ import com.sparrowwallet.drongo.protocol.Base58;
|
|||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -35,10 +40,11 @@ public class CardApi {
|
|||
return cardProtocol.getStatus();
|
||||
}
|
||||
|
||||
void checkWait(CardStatus cardStatus, StringProperty messageProperty) throws CardException {
|
||||
void checkWait(CardStatus cardStatus, IntegerProperty delayProperty, StringProperty messageProperty) throws CardException {
|
||||
if(cardStatus.auth_delay != null) {
|
||||
int delay = cardStatus.auth_delay.intValue();
|
||||
while(delay > 0) {
|
||||
delayProperty.set(delay);
|
||||
messageProperty.set("Auth delay, waiting " + delay + "s...");
|
||||
CardWait cardWait = cardProtocol.authWait();
|
||||
if(cardWait.success) {
|
||||
|
@ -48,6 +54,38 @@ public class CardApi {
|
|||
}
|
||||
}
|
||||
|
||||
public Service<Void> getAuthDelayService() throws CardException {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.auth_delay != null) {
|
||||
return new AuthDelayService(cardStatus);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean requiresBackup() throws CardException {
|
||||
CardStatus cardStatus = getStatus();
|
||||
return cardStatus.requiresBackup();
|
||||
}
|
||||
|
||||
public Service<String> getBackupService() {
|
||||
return new BackupService();
|
||||
}
|
||||
|
||||
String getBackup() throws CardException {
|
||||
CardBackup cardBackup = cardProtocol.backup(cvc);
|
||||
return Utils.bytesToHex(cardBackup.data);
|
||||
}
|
||||
|
||||
public boolean changePin(String newCvc) throws CardException {
|
||||
CardChange cardChange = cardProtocol.change(cvc, newCvc);
|
||||
if(cardChange.success) {
|
||||
cvc = newCvc;
|
||||
}
|
||||
|
||||
return cardChange.success;
|
||||
}
|
||||
|
||||
public void setDerivation(List<ChildNumber> derivation) throws CardException {
|
||||
cardProtocol.derive(cvc, derivation);
|
||||
}
|
||||
|
@ -81,4 +119,45 @@ public class CardApi {
|
|||
log.warn("Error disconnecting from card reader", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isReaderAvailable() {
|
||||
return CardTransport.isReaderAvailable();
|
||||
}
|
||||
|
||||
public class AuthDelayService extends Service<Void> {
|
||||
private final CardStatus cardStatus;
|
||||
private final IntegerProperty delayProperty;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
AuthDelayService(CardStatus cardStatus) {
|
||||
this.cardStatus = cardStatus;
|
||||
this.delayProperty = new SimpleIntegerProperty();
|
||||
this.messageProperty = new SimpleStringProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
delayProperty.addListener((observable, oldValue, newValue) -> updateProgress(cardStatus.auth_delay.intValue() - newValue.intValue(), cardStatus.auth_delay.intValue()));
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> updateMessage(newValue));
|
||||
checkWait(cardStatus, delayProperty, messageProperty);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class BackupService extends Service<String> {
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
return getBackup();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ public class CardStatus extends CardResponse {
|
|||
return null;
|
||||
}
|
||||
|
||||
public boolean requiresBackup() {
|
||||
return num_backups == null || num_backups.intValue() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CardStatus{" +
|
||||
|
|
|
@ -32,7 +32,7 @@ public class CardTransport {
|
|||
|
||||
private final Card connection;
|
||||
|
||||
public CardTransport() throws CardException {
|
||||
CardTransport() throws CardException {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
List<CardTerminal> terminals = tf.terminals().list();
|
||||
if(terminals.isEmpty()) {
|
||||
|
@ -49,7 +49,7 @@ public class CardTransport {
|
|||
}
|
||||
}
|
||||
|
||||
public JsonObject send(String cmd, Map<String, Object> args) throws CardException {
|
||||
JsonObject send(String cmd, Map<String, Object> args) throws CardException {
|
||||
Map<String, Object> sendMap = new LinkedHashMap<>();
|
||||
sendMap.put("cmd", cmd);
|
||||
sendMap.putAll(args);
|
||||
|
@ -106,11 +106,10 @@ public class CardTransport {
|
|||
if(result.get("error") != null) {
|
||||
String msg = result.get("error").getAsString();
|
||||
int code = result.get("code") == null ? 500 : result.get("code").getAsInt();
|
||||
String message = code + " on " + cmd + ": " + msg;
|
||||
if(code == 205) {
|
||||
throw new CardUnluckyNumberException(message);
|
||||
throw new CardUnluckyNumberException("Card chose unlucky number, please retry.");
|
||||
} else if(code == 401) {
|
||||
throw new CardAuthorizationException(message);
|
||||
throw new CardAuthorizationException("Incorrect PIN provided.");
|
||||
}
|
||||
|
||||
throw new CardException(code + " on " + cmd + ": " + msg);
|
||||
|
@ -146,11 +145,11 @@ public class CardTransport {
|
|||
throw new IllegalArgumentException("Cannot convert dataItem of type " + dataItem.getClass() + "to JsonElement");
|
||||
}
|
||||
|
||||
public void disconnect() throws CardException {
|
||||
void disconnect() throws CardException {
|
||||
connection.disconnect(true);
|
||||
}
|
||||
|
||||
public static boolean isReaderAvailable() {
|
||||
static boolean isReaderAvailable() {
|
||||
try {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
return !tf.terminals().list().isEmpty();
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.wallet.Keystore;
|
|||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
|
@ -32,7 +33,7 @@ public class CkCard implements KeystoreCardImport {
|
|||
cardApi.initialize();
|
||||
cardStatus = cardApi.getStatus();
|
||||
}
|
||||
cardApi.checkWait(cardStatus, messageProperty);
|
||||
cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
|
||||
if(!derivation.equals(cardStatus.getDerivation())) {
|
||||
cardApi.setDerivation(derivation);
|
||||
|
|
|
@ -11,11 +11,12 @@ import javafx.scene.control.Accordion;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.TerminalFactory;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.sparrow.io.ckcard.CardApi.isReaderAvailable;
|
||||
|
||||
public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
private static final Logger log = LoggerFactory.getLogger(HwAirgappedController.class);
|
||||
|
||||
|
@ -54,15 +55,4 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
|||
|
||||
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
}
|
||||
|
||||
public static boolean isReaderAvailable() {
|
||||
try {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
return !tf.terminals().list().isEmpty();
|
||||
} catch(Exception e) {
|
||||
log.error("Error detecting card terminals", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,10 @@ import com.sparrowwallet.sparrow.event.*;
|
|||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.CardApi;
|
||||
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
|
@ -29,11 +31,14 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.net.URL;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.io.ckcard.CardApi.isReaderAvailable;
|
||||
|
||||
public class KeystoreController extends WalletFormController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(KeystoreController.class);
|
||||
|
||||
|
@ -56,6 +61,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
@FXML
|
||||
private Button viewKeyButton;
|
||||
|
||||
@FXML
|
||||
private Button changePinButton;
|
||||
|
||||
@FXML
|
||||
private Button importButton;
|
||||
|
||||
|
@ -117,6 +125,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
|
||||
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
|
||||
viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty());
|
||||
changePinButton.managedProperty().bind(changePinButton.visibleProperty());
|
||||
scanXpubQR.managedProperty().bind(scanXpubQR.visibleProperty());
|
||||
displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty());
|
||||
displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not());
|
||||
|
@ -273,6 +282,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
type.setGraphic(getTypeIcon(keystore));
|
||||
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
|
||||
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
|
||||
changePinButton.setVisible(keystore.getWalletModel() == WalletModel.TAPSIGNER);
|
||||
|
||||
importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace...");
|
||||
importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));
|
||||
|
@ -404,6 +414,84 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
}
|
||||
}
|
||||
|
||||
public void changeCardPin(ActionEvent event) {
|
||||
if(!isReaderAvailable()) {
|
||||
AppServices.showErrorDialog("No card reader", "Connect a card reader to change the card PIN.");
|
||||
return;
|
||||
}
|
||||
|
||||
CardPinDialog cardPinDialog = new CardPinDialog();
|
||||
Optional<CardPinDialog.CardPinChange> optPinChange = cardPinDialog.showAndWait();
|
||||
if(optPinChange.isPresent()) {
|
||||
String currentPin = optPinChange.get().currentPin();
|
||||
String newPin = optPinChange.get().newPin();
|
||||
boolean backupFirst = optPinChange.get().backupFirst();
|
||||
try {
|
||||
CardApi cardApi = new CardApi(currentPin);
|
||||
Service<Void> authDelayService = cardApi.getAuthDelayService();
|
||||
if(authDelayService != null) {
|
||||
authDelayService.setOnSucceeded(event1 -> {
|
||||
try {
|
||||
changeCardPin(newPin, backupFirst, cardApi);
|
||||
} catch(CardException e) {
|
||||
log.error("Error communicating with card", e);
|
||||
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
|
||||
}
|
||||
});
|
||||
authDelayService.setOnFailed(event1 -> {
|
||||
Throwable e = event1.getSource().getException();
|
||||
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);
|
||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||
authDelayService.start();
|
||||
} else {
|
||||
changeCardPin(newPin, backupFirst, cardApi);
|
||||
}
|
||||
} catch(CardException e) {
|
||||
log.error("Error communicating with card", e);
|
||||
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void changeCardPin(String newPin, boolean backupFirst, CardApi cardApi) throws CardException {
|
||||
boolean requiresBackup = cardApi.requiresBackup();
|
||||
if(backupFirst || requiresBackup) {
|
||||
Service<String> backupService = cardApi.getBackupService();
|
||||
backupService.setOnSucceeded(event -> {
|
||||
String backup = backupService.getValue();
|
||||
TextAreaDialog backupDialog = new TextAreaDialog(backup, false);
|
||||
backupDialog.setTitle("Backup Private Key");
|
||||
backupDialog.getDialogPane().setHeaderText((requiresBackup ? "Please backup first by saving" : "Save") + " the following text in a safe place. It contains an encrypted copy of the card's private key, and can be decrypted using the backup key written on the back of the card.");
|
||||
backupDialog.showAndWait();
|
||||
try {
|
||||
changePin(newPin, cardApi);
|
||||
} catch(Exception e) {
|
||||
log.error("Error communicating with card", e);
|
||||
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
|
||||
}
|
||||
});
|
||||
backupService.setOnFailed(event -> {
|
||||
Throwable e = event.getSource().getException();
|
||||
log.error("Error communicating with card", e);
|
||||
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
|
||||
});
|
||||
backupService.start();
|
||||
} else {
|
||||
changePin(newPin, cardApi);
|
||||
}
|
||||
}
|
||||
|
||||
private void changePin(String newPin, CardApi cardApi) throws CardException {
|
||||
if(cardApi.changePin(newPin)) {
|
||||
AppServices.showSuccessDialog("PIN changed", "The card's PIN has been changed.");
|
||||
} else {
|
||||
AppServices.showSuccessDialog("Could not change PIN", "The card's PIN was not changed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void scanXpubQR(ActionEvent event) {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
|
|
|
@ -40,6 +40,14 @@
|
|||
<Tooltip text="View master private key"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Button fx:id="changePinButton" text="Change Pin..." graphicTextGap="5" onAction="#changeCardPin">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="WIFI" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Change the PIN of current card"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Pane HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="importButton" text="Import..." graphicTextGap="5" onAction="#importKeystore">
|
||||
<graphic>
|
||||
|
|
Loading…
Reference in a new issue