Add wallet restore to pw popup

This commit is contained in:
Manfred Karrer 2016-04-11 23:55:38 +02:00
parent a823c0360e
commit 97c54eb58e
3 changed files with 245 additions and 37 deletions

View file

@ -396,7 +396,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
model.p2PNetworkIconId.addListener(splashP2PNetworkIconIdListener);
splashP2PNetworkProgressListener = (ov, oldValue, newValue) -> {
if ((double) newValue >= 1) {
if ((double) newValue != -1) {
splashP2PNetworkIndicator.setVisible(false);
splashP2PNetworkIndicator.setManaged(false);
}

View file

@ -17,36 +17,71 @@
package io.bitsquare.gui.main.overlays.windows;
import com.google.common.base.Splitter;
import io.bitsquare.app.BitsquareApp;
import io.bitsquare.btc.WalletService;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Tuple2;
import io.bitsquare.crypto.ScryptUtil;
import io.bitsquare.gui.components.PasswordTextField;
import io.bitsquare.gui.main.overlays.Overlay;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.util.Transitions;
import io.bitsquare.gui.util.validation.PasswordValidator;
import io.bitsquare.locale.BSResources;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.bitcoinj.core.Wallet;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.crypto.MnemonicCode;
import org.bitcoinj.crypto.MnemonicException;
import org.bitcoinj.wallet.DeterministicSeed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.inject.Inject;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;
import static com.google.inject.internal.util.$Preconditions.checkArgument;
import static io.bitsquare.gui.util.FormBuilder.*;
import static javafx.beans.binding.Bindings.createBooleanBinding;
public class WalletPasswordWindow extends Overlay<WalletPasswordWindow> {
private static final Logger log = LoggerFactory.getLogger(WalletPasswordWindow.class);
private final WalletService walletService;
private Button unlockButton;
private AesKeyHandler aesKeyHandler;
private PasswordTextField passwordTextField;
private Button forgotPasswordButton;
private Button restoreButton;
private TextArea restoreSeedWordsTextArea;
private DatePicker restoreDatePicker;
private SimpleBooleanProperty seedWordsValid = new SimpleBooleanProperty(false);
private SimpleBooleanProperty dateValid = new SimpleBooleanProperty(false);
private BooleanProperty seedWordsEdited = new SimpleBooleanProperty();
private ChangeListener<String> changeListener;
private ChangeListener<String> seedWordsTextAreaChangeListener;
private ChangeListener<Boolean> datePickerChangeListener;
private ChangeListener<Boolean> seedWordsValidChangeListener;
private ChangeListener<LocalDate> dateChangeListener;
private LocalDate walletCreationDate;
///////////////////////////////////////////////////////////////////////////////////////////
@ -57,23 +92,19 @@ public class WalletPasswordWindow extends Overlay<WalletPasswordWindow> {
void onAesKey(KeyParameter aesKey);
}
@Inject
public WalletPasswordWindow(WalletService walletService) {
this.walletService = walletService;
type = Type.Attention;
width = 800;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void cleanup() {
if (passwordTextField != null)
passwordTextField.textProperty().addListener(changeListener);
}
@Inject
public WalletPasswordWindow(WalletService walletService) {
this.walletService = walletService;
type = Type.Attention;
}
public void show() {
if (gridPane != null) {
rowIndex = -1;
@ -97,11 +128,42 @@ public class WalletPasswordWindow extends Overlay<WalletPasswordWindow> {
return this;
}
@Override
protected void cleanup() {
if (passwordTextField != null)
passwordTextField.textProperty().removeListener(changeListener);
if (seedWordsValidChangeListener != null) {
seedWordsValid.removeListener(seedWordsValidChangeListener);
dateValid.removeListener(datePickerChangeListener);
restoreSeedWordsTextArea.textProperty().removeListener(seedWordsTextAreaChangeListener);
restoreDatePicker.valueProperty().removeListener(dateChangeListener);
restoreButton.disableProperty().unbind();
restoreButton.setOnAction(null);
restoreSeedWordsTextArea.setText("");
restoreDatePicker.setValue(null);
restoreSeedWordsTextArea.getStyleClass().remove("validation_error");
restoreDatePicker.getStyleClass().remove("validation_error");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void setupKeyHandler(Scene scene) {
if (!hideCloseButton) {
scene.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ESCAPE) {
e.consume();
doClose();
}
});
}
}
private void addInputFields() {
Label label = new Label("Enter password:");
label.setWrapText(true);
@ -113,9 +175,7 @@ public class WalletPasswordWindow extends Overlay<WalletPasswordWindow> {
GridPane.setRowIndex(passwordTextField, rowIndex);
GridPane.setColumnIndex(passwordTextField, 1);
PasswordValidator passwordValidator = new PasswordValidator();
changeListener = (observable, oldValue, newValue) -> {
unlockButton.setDisable(!passwordValidator.validate(newValue).isValid);
};
changeListener = (observable, oldValue, newValue) -> unlockButton.setDisable(!passwordValidator.validate(newValue).isValid);
passwordTextField.textProperty().addListener(changeListener);
gridPane.getChildren().addAll(label, passwordTextField);
}
@ -124,7 +184,36 @@ public class WalletPasswordWindow extends Overlay<WalletPasswordWindow> {
unlockButton = new Button("Unlock");
unlockButton.setDefaultButton(true);
unlockButton.setDisable(true);
unlockButton.setOnAction(e -> checkPassword());
unlockButton.setOnAction(e -> {
String password = passwordTextField.getText();
checkArgument(password.length() < 50, "Password must be less then 50 characters.");
Wallet wallet = walletService.getWallet();
KeyCrypterScrypt keyCrypterScrypt = (KeyCrypterScrypt) wallet.getKeyCrypter();
if (keyCrypterScrypt != null) {
ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> {
if (wallet.checkAESKey(aesKey)) {
if (aesKeyHandler != null)
aesKeyHandler.onAesKey(aesKey);
hide();
} else {
UserThread.runAfter(() -> new Popup()
.warning("You entered the wrong password.\n\n" +
"Please try entering your password again, carefully checking for typos or spelling errors.")
.onClose(this::blurAgain).show(), Transitions.DEFAULT_DURATION, TimeUnit.MILLISECONDS);
}
});
} else {
log.error("wallet.getKeyCrypter() is null, than must not happen.");
}
});
forgotPasswordButton = new Button("Forgot password?");
forgotPasswordButton.setOnAction(e -> {
forgotPasswordButton.setDisable(true);
unlockButton.setDefaultButton(false);
showRestoreScreen();
});
Button cancelButton = new Button("Cancel");
cancelButton.setOnAction(event -> {
@ -137,32 +226,150 @@ public class WalletPasswordWindow extends Overlay<WalletPasswordWindow> {
GridPane.setRowIndex(hBox, ++rowIndex);
GridPane.setColumnIndex(hBox, 1);
if (hideCloseButton)
hBox.getChildren().add(unlockButton);
hBox.getChildren().addAll(unlockButton, forgotPasswordButton);
else
hBox.getChildren().addAll(unlockButton, cancelButton);
gridPane.getChildren().add(hBox);
ColumnConstraints columnConstraints1 = new ColumnConstraints();
columnConstraints1.setHalignment(HPos.RIGHT);
columnConstraints1.setHgrow(Priority.SOMETIMES);
ColumnConstraints columnConstraints2 = new ColumnConstraints();
columnConstraints2.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2);
}
private void checkPassword() {
String password = passwordTextField.getText();
Wallet wallet = walletService.getWallet();
KeyCrypterScrypt keyCrypterScrypt = (KeyCrypterScrypt) wallet.getKeyCrypter();
if (keyCrypterScrypt != null) {
ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> {
if (wallet.checkAESKey(aesKey)) {
if (aesKeyHandler != null)
aesKeyHandler.onAesKey(aesKey);
private void showRestoreScreen() {
Label headLine2Label = new Label(BSResources.get("Restore wallet from seed words"));
headLine2Label.setId("popup-headline");
headLine2Label.setMouseTransparent(true);
GridPane.setHalignment(headLine2Label, HPos.LEFT);
GridPane.setRowIndex(headLine2Label, ++rowIndex);
GridPane.setColumnSpan(headLine2Label, 2);
GridPane.setMargin(headLine2Label, new Insets(30, 0, 0, 0));
gridPane.getChildren().add(headLine2Label);
hide();
} else {
UserThread.runAfter(() -> new Popup()
.warning("You entered the wrong password.\n\n" +
"Please try entering your password again, carefully checking for typos or spelling errors.")
.onClose(this::blurAgain).show(), Transitions.DEFAULT_DURATION, TimeUnit.MILLISECONDS);
}
});
Separator separator = new Separator();
separator.setMouseTransparent(true);
separator.setOrientation(Orientation.HORIZONTAL);
separator.setStyle("-fx-background: #ccc;");
GridPane.setHalignment(separator, HPos.CENTER);
GridPane.setRowIndex(separator, ++rowIndex);
GridPane.setColumnSpan(separator, 2);
gridPane.getChildren().add(separator);
Tuple2<Label, TextArea> tuple = addLabelTextArea(gridPane, ++rowIndex, "Wallet seed words:", "", 5);
restoreSeedWordsTextArea = tuple.second;
restoreSeedWordsTextArea.setPrefHeight(60);
restoreSeedWordsTextArea.setStyle("-fx-border-color: #ddd;");
Tuple2<Label, DatePicker> labelDatePickerTuple2 = addLabelDatePicker(gridPane, ++rowIndex, "Creation Date:");
restoreDatePicker = labelDatePickerTuple2.second;
restoreButton = addButton(gridPane, ++rowIndex, "Restore wallet");
restoreButton.setDefaultButton(true);
stage.setHeight(340);
DeterministicSeed keyChainSeed = walletService.getWallet().getKeyChainSeed();
// wallet creation date is not encrypted
walletCreationDate = Instant.ofEpochSecond(keyChainSeed.getCreationTimeSeconds()).atZone(ZoneId.systemDefault()).toLocalDate();
restoreButton.disableProperty().bind(createBooleanBinding(() -> !seedWordsValid.get() || !dateValid.get() || !seedWordsEdited.get(),
seedWordsValid, dateValid, seedWordsEdited));
seedWordsValidChangeListener = (observable, oldValue, newValue) -> {
if (newValue) {
restoreSeedWordsTextArea.getStyleClass().remove("validation_error");
} else {
restoreSeedWordsTextArea.getStyleClass().add("validation_error");
}
};
seedWordsTextAreaChangeListener = (observable, oldValue, newValue) -> {
seedWordsEdited.set(true);
try {
MnemonicCode codec = new MnemonicCode();
codec.check(Splitter.on(" ").splitToList(newValue));
seedWordsValid.set(true);
} catch (IOException | MnemonicException e) {
seedWordsValid.set(false);
}
};
datePickerChangeListener = (observable, oldValue, newValue) -> {
if (newValue)
restoreDatePicker.getStyleClass().remove("validation_error");
else
restoreDatePicker.getStyleClass().add("validation_error");
};
dateChangeListener = (observable, oldValue, newValue) -> {
dateValid.set(walletCreationDate.equals(newValue));
};
seedWordsValid.addListener(seedWordsValidChangeListener);
dateValid.addListener(datePickerChangeListener);
restoreSeedWordsTextArea.textProperty().addListener(seedWordsTextAreaChangeListener);
restoreDatePicker.valueProperty().addListener(dateChangeListener);
restoreButton.disableProperty().bind(createBooleanBinding(() -> !seedWordsValid.get() || !dateValid.get() || !seedWordsEdited.get(),
seedWordsValid, dateValid, seedWordsEdited));
restoreButton.setOnAction(e -> onRestore());
restoreSeedWordsTextArea.getStyleClass().remove("validation_error");
restoreDatePicker.getStyleClass().remove("validation_error");
layout();
}
private void onRestore() {
Wallet wallet = walletService.getWallet();
if (wallet.getBalance(Wallet.BalanceType.AVAILABLE).value > 0) {
new Popup()
.warning("Your bitcoin wallet is not empty.\n\n" +
"You must empty this wallet before attempting to restore an older one, as mixing wallets " +
"together can lead to invalidated backups.\n\n" +
"Please finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\n" +
"In case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\n" +
"To open that emergency tool press \"cmd + e\".")
.show();
} else if (wallet.isEncrypted()) {
new Popup()
.warning("Your bitcoin wallet is encrypted.\n\n" +
"After restore, the wallet will no longer be encrypted and you must set a new password.\n\n" +
"Do you want to proceed?")
.closeButtonText("No")
.actionButtonText("Yes")
.onAction(this::doRestore)
.show();
} else {
log.error("wallet.getKeyCrypter() is null, than must not happen.");
doRestore();
}
}
private void doRestore() {
log.info("Attempting wallet restore using seed '{}' from date {}", restoreSeedWordsTextArea.getText(), restoreDatePicker.getValue());
long date = restoreDatePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC);
DeterministicSeed seed = new DeterministicSeed(Splitter.on(" ").splitToList(restoreSeedWordsTextArea.getText()), null, "", date);
walletService.restoreSeedWords(seed,
() -> UserThread.execute(() -> {
log.debug("Wallet restored with seed words");
new Popup()
.feedback("Wallet restored successfully with the new seed words.\n\n" +
"You need to shut down and restart the application.")
.closeButtonText("Shut down")
.onClose(BitsquareApp.shutDownHandler::run)
.show();
}),
throwable -> UserThread.execute(() -> {
log.error(throwable.getMessage());
new Popup()
.error("An error occurred when restoring the wallet with seed words.\n" +
"Error message: " + throwable.getMessage())
.show();
}));
}
}

View file

@ -26,6 +26,7 @@ validation.fiat.toLarge=Input larger as maximum possible amount is not allowed.
validation.btc.toSmall=Input results in a bitcoin value with a fraction of the smallest unit (Satoshi).
validation.btc.toLarge=Input larger as maximum trading amount of {0} is not allowed.
validation.passwordTooShort=The password you entered is too short. It needs to have min. 8 characters.
validation.passwordTooLong=The password you entered is too long. It cannot be longer as 50 characters.
# Create offer
createOffer.amount.prompt=Enter amount in BTC