/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
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.BusyAnimation;
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.geometry.Orientation;
import javafx.geometry.Pos;
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;
///////////////////////////////////////////////////////////////////////////////////////////
// Interface
///////////////////////////////////////////////////////////////////////////////////////////
public interface AesKeyHandler {
void onAesKey(KeyParameter aesKey);
}
@Inject
public WalletPasswordWindow(WalletService walletService) {
this.walletService = walletService;
type = Type.Attention;
width = 800;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void show() {
if (gridPane != null) {
rowIndex = -1;
gridPane.getChildren().clear();
}
if (headLine == null)
headLine = "Enter password to unlock";
createGridPane();
addHeadLine();
addSeparator();
addInputFields();
addButtons();
applyStyles();
display();
}
public WalletPasswordWindow onAesKey(AesKeyHandler aesKeyHandler) {
this.aesKeyHandler = aesKeyHandler;
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);
GridPane.setMargin(label, new Insets(3, 0, 0, 0));
GridPane.setRowIndex(label, ++rowIndex);
passwordTextField = new PasswordTextField();
GridPane.setMargin(passwordTextField, new Insets(3, 0, 0, 0));
GridPane.setRowIndex(passwordTextField, rowIndex);
GridPane.setColumnIndex(passwordTextField, 1);
PasswordValidator passwordValidator = new PasswordValidator();
changeListener = (observable, oldValue, newValue) -> unlockButton.setDisable(!passwordValidator.validate(newValue).isValid);
passwordTextField.textProperty().addListener(changeListener);
gridPane.getChildren().addAll(label, passwordTextField);
}
private void addButtons() {
BusyAnimation busyAnimation = new BusyAnimation(false);
Label deriveStatusLabel = new Label();
unlockButton = new Button("Unlock");
unlockButton.setDefaultButton(true);
unlockButton.setDisable(true);
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) {
busyAnimation.play();
deriveStatusLabel.setText("Derive key from password");
ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> {
if (wallet.checkAESKey(aesKey)) {
if (aesKeyHandler != null)
aesKeyHandler.onAesKey(aesKey);
hide();
} else {
busyAnimation.stop();
deriveStatusLabel.setText("");
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, that 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 -> {
hide();
closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run());
});
HBox hBox = new HBox();
hBox.setMinWidth(560);
hBox.setSpacing(10);
GridPane.setRowIndex(hBox, ++rowIndex);
GridPane.setColumnIndex(hBox, 1);
hBox.setAlignment(Pos.CENTER_LEFT);
if (hideCloseButton)
hBox.getChildren().addAll(unlockButton, forgotPasswordButton, busyAnimation, deriveStatusLabel);
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 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);
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\".")
.actionButtonText("I want to restore anyway")
.onAction(this::checkIfEncrypted)
.closeButtonText("I will empty my wallet first")
.show();
} else {
checkIfEncrypted();
}
}
private void checkIfEncrypted() {
if (walletService.getWallet().isEncrypted()) {
new Popup()
.information("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 {
doRestore();
}
}
private void doRestore() {
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();
}));
}
}