3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 06:44:16 +00:00

WalletTemplate: add support for encryption. Scrypt parameters are calculated by testing CPU speed. A pie chart shows smooth decryption progress.

This commit is contained in:
Mike Hearn 2014-07-13 20:04:08 +02:00
parent d6cf090f5c
commit 29a11e22b7
12 changed files with 676 additions and 79 deletions

View File

@ -1,6 +1,9 @@
package wallettemplate; package wallettemplate;
import com.google.bitcoin.core.*; import com.google.bitcoin.core.AbstractWalletEventListener;
import com.google.bitcoin.core.Coin;
import com.google.bitcoin.core.DownloadListener;
import com.google.bitcoin.core.Wallet;
import javafx.animation.*; import javafx.animation.*;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
@ -47,7 +50,8 @@ public class Controller {
} }
public void settingsClicked(ActionEvent event) { public void settingsClicked(ActionEvent event) {
Main.instance.overlayUI("wallet_settings.fxml"); Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("wallet_settings.fxml");
screen.controller.initialize(null);
} }
public class ProgressBarUpdater extends DownloadListener { public class ProgressBarUpdater extends DownloadListener {
@ -104,6 +108,7 @@ public class Controller {
public void onWalletChanged(Wallet wallet) { public void onWalletChanged(Wallet wallet) {
checkGuiThread(); checkGuiThread();
refreshBalanceLabel(); refreshBalanceLabel();
// TODO: Refresh clickable address here.
} }
} }

View File

@ -28,7 +28,7 @@ import static wallettemplate.utils.GuiUtils.*;
public class Main extends Application { public class Main extends Application {
public static String APP_NAME = "WalletTemplate"; public static String APP_NAME = "WalletTemplate";
public static NetworkParameters params = MainNetParams.get(); public static NetworkParameters params = RegTestParams.get();
public static WalletAppKit bitcoin; public static WalletAppKit bitcoin;
public static Main instance; public static Main instance;
@ -90,7 +90,8 @@ public class Main extends Application {
// Don't make the user wait for confirmations for now, as the intention is they're sending it // Don't make the user wait for confirmations for now, as the intention is they're sending it
// their own money! // their own money!
bitcoin.wallet().allowSpendingUnconfirmedTransactions(); bitcoin.wallet().allowSpendingUnconfirmedTransactions();
bitcoin.peerGroup().setMaxConnections(11); if (params != RegTestParams.get())
bitcoin.peerGroup().setMaxConnections(11);
bitcoin.peerGroup().setBloomFilterFalsePositiveRate(0.00001); bitcoin.peerGroup().setBloomFilterFalsePositiveRate(0.00001);
Platform.runLater(controller::onBitcoinSetup); Platform.runLater(controller::onBitcoinSetup);
} }
@ -115,6 +116,8 @@ public class Main extends Application {
bitcoin.restoreWalletFromSeed(seed); bitcoin.restoreWalletFromSeed(seed);
} }
private Node stopClickPane = new Pane();
public class OverlayUI<T> { public class OverlayUI<T> {
public Node ui; public Node ui;
public T controller; public T controller;
@ -125,26 +128,53 @@ public class Main extends Application {
} }
public void show() { public void show() {
blurOut(mainUI); checkGuiThread();
uiStack.getChildren().add(ui); if (currentOverlay == null) {
fadeIn(ui); uiStack.getChildren().add(stopClickPane);
uiStack.getChildren().add(ui);
blurOut(mainUI);
//darken(mainUI);
fadeIn(ui);
zoomIn(ui);
} else {
// Do a quick transition between the current overlay and the next.
// Bug here: we don't pay attention to changes in outsideClickDismisses.
explodeOut(currentOverlay.ui);
fadeOutAndRemove(uiStack, currentOverlay.ui);
uiStack.getChildren().add(ui);
ui.setOpacity(0.0);
fadeIn(ui, 100);
zoomIn(ui, 100);
}
currentOverlay = this;
}
public void outsideClickDismisses() {
stopClickPane.setOnMouseClicked((ev) -> done());
} }
public void done() { public void done() {
checkGuiThread(); checkGuiThread();
fadeOutAndRemove(ui, uiStack); if (ui == null) return; // In the middle of being dismissed and got an extra click.
explodeOut(ui);
fadeOutAndRemove(uiStack, ui, stopClickPane);
blurIn(mainUI); blurIn(mainUI);
//undark(mainUI);
this.ui = null; this.ui = null;
this.controller = null; this.controller = null;
currentOverlay = null;
} }
} }
@Nullable
private OverlayUI currentOverlay;
public <T> OverlayUI<T> overlayUI(Node node, T controller) { public <T> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread(); checkGuiThread();
OverlayUI<T> pair = new OverlayUI<T>(node, controller); OverlayUI<T> pair = new OverlayUI<T>(node, controller);
// Auto-magically set the overlayUi member, if it's there. // Auto-magically set the overlayUI member, if it's there.
try { try {
controller.getClass().getDeclaredField("overlayUi").set(controller, pair); controller.getClass().getField("overlayUI").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) { } catch (IllegalAccessException | NoSuchFieldException ignored) {
} }
pair.show(); pair.show();
@ -156,15 +186,17 @@ public class Main extends Application {
try { try {
checkGuiThread(); checkGuiThread();
// Load the UI from disk. // Load the UI from disk.
URL location = getClass().getResource(name); URL location = GuiUtils.getResource(name);
FXMLLoader loader = new FXMLLoader(location); FXMLLoader loader = new FXMLLoader(location);
Pane ui = loader.load(); Pane ui = loader.load();
T controller = loader.getController(); T controller = loader.getController();
OverlayUI<T> pair = new OverlayUI<T>(ui, controller); OverlayUI<T> pair = new OverlayUI<T>(ui, controller);
// Auto-magically set the overlayUi member, if it's there. // Auto-magically set the overlayUI member, if it's there.
try { try {
controller.getClass().getDeclaredField("overlayUi").set(controller, pair); if (controller != null)
controller.getClass().getField("overlayUI").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) { } catch (IllegalAccessException | NoSuchFieldException ignored) {
ignored.printStackTrace();
} }
pair.show(); pair.show();
return pair; return pair;

View File

@ -3,15 +3,15 @@ package wallettemplate;
import com.google.bitcoin.core.*; import com.google.bitcoin.core.*;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import javafx.application.Platform;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import org.spongycastle.crypto.params.KeyParameter;
import wallettemplate.controls.BitcoinAddressValidator; import wallettemplate.controls.BitcoinAddressValidator;
import static wallettemplate.utils.GuiUtils.crashAlert; import static com.google.common.base.Preconditions.checkState;
import static wallettemplate.utils.GuiUtils.informationalAlert; import static wallettemplate.utils.GuiUtils.*;
public class SendMoneyController { public class SendMoneyController {
public Button sendBtn; public Button sendBtn;
@ -19,28 +19,33 @@ public class SendMoneyController {
public TextField address; public TextField address;
public Label titleLabel; public Label titleLabel;
public Main.OverlayUI overlayUi; public Main.OverlayUI overlayUI;
private Wallet.SendResult sendResult; private Wallet.SendResult sendResult;
private KeyParameter aesKey;
// Called by FXMLLoader // Called by FXMLLoader
public void initialize() { public void initialize() {
checkState(!Main.bitcoin.wallet().getBalance().isZero());
new BitcoinAddressValidator(Main.params, address, sendBtn); new BitcoinAddressValidator(Main.params, address, sendBtn);
} }
public void cancel(ActionEvent event) { public void cancel(ActionEvent event) {
overlayUi.done(); overlayUI.done();
} }
public void send(ActionEvent event) { public void send(ActionEvent event) {
// Address exception cannot happen as we validated it beforehand.
try { try {
Address destination = new Address(Main.params, address.getText()); Address destination = new Address(Main.params, address.getText());
Wallet.SendRequest req = Wallet.SendRequest.emptyWallet(destination); Wallet.SendRequest req = Wallet.SendRequest.emptyWallet(destination);
req.aesKey = aesKey;
sendResult = Main.bitcoin.wallet().sendCoins(req); sendResult = Main.bitcoin.wallet().sendCoins(req);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() { Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
@Override @Override
public void onSuccess(Transaction result) { public void onSuccess(Transaction result) {
Platform.runLater(overlayUi::done); checkGuiThread();
overlayUI.done();
} }
@Override @Override
@ -56,16 +61,32 @@ public class SendMoneyController {
sendBtn.setDisable(true); sendBtn.setDisable(true);
address.setDisable(true); address.setDisable(true);
updateTitleForBroadcast(); updateTitleForBroadcast();
} catch (AddressFormatException e) {
// Cannot happen because we already validated it when the text field changed.
throw new RuntimeException(e);
} catch (InsufficientMoneyException e) { } catch (InsufficientMoneyException e) {
informationalAlert("Could not empty the wallet", informationalAlert("Could not empty the wallet",
"You may have too little money left in the wallet to make a transaction."); "You may have too little money left in the wallet to make a transaction.");
overlayUi.done(); overlayUI.done();
} catch (ECKey.KeyIsEncryptedException e) {
askForPasswordAndRetry();
} catch (AddressFormatException e) {
// Cannot happen because we already validated it when the text field changed.
throw new RuntimeException(e);
} }
} }
private void askForPasswordAndRetry() {
Main.OverlayUI<WalletPasswordController> pwd = Main.instance.overlayUI("wallet_password.fxml");
final String addressStr = address.getText();
pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
// We only get here if the user found the right password. If they don't or they cancel, we end up back on
// the main UI screen. By now the send money screen is history so we must recreate it.
checkGuiThread();
Main.OverlayUI<SendMoneyController> screen = Main.instance.overlayUI("send_money.fxml");
screen.controller.aesKey = cur;
screen.controller.address.setText(addressStr);
screen.controller.send(null);
});
}
private void updateTitleForBroadcast() { private void updateTitleForBroadcast() {
final int peers = sendResult.tx.getConfidence().numBroadcastPeers(); final int peers = sendResult.tx.getConfidence().numBroadcastPeers();
titleLabel.setText(String.format("Broadcasting ... seen by %d peers", peers)); titleLabel.setText(String.format("Broadcasting ... seen by %d peers", peers));

View File

@ -0,0 +1,106 @@
package wallettemplate;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.common.primitives.Longs;
import com.google.protobuf.ByteString;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import wallettemplate.utils.KeyDerivationTasks;
import java.time.Duration;
import static com.google.common.base.Preconditions.checkNotNull;
import static wallettemplate.utils.GuiUtils.*;
/**
* User interface for entering a password on demand, e.g. to send money. Also used when encrypting a wallet. Shows a
* progress meter as we scrypt the password.
*/
public class WalletPasswordController {
private static final Logger log = LoggerFactory.getLogger(WalletPasswordController.class);
@FXML HBox buttonsBox;
@FXML PasswordField pass1;
@FXML ImageView padlockImage;
@FXML ProgressIndicator progressMeter;
@FXML GridPane widgetGrid;
@FXML Label explanationLabel;
public Main.OverlayUI overlayUI;
private SimpleObjectProperty<KeyParameter> aesKey = new SimpleObjectProperty<>();
public void initialize() {
progressMeter.setOpacity(0);
}
@FXML void confirmClicked(ActionEvent event) {
String password = pass1.getText();
if (password.isEmpty() || password.length() < 4) {
informationalAlert("Bad password", "The password you entered is empty or too short.");
return;
}
final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) Main.bitcoin.wallet().getKeyCrypter();
checkNotNull(keyCrypter); // We should never arrive at this GUI if the wallet isn't actually encrypted.
KeyDerivationTasks tasks = new KeyDerivationTasks(keyCrypter, password, getTargetTime()) {
@Override
protected void onFinish(KeyParameter aesKey) {
super.onFinish(aesKey);
checkGuiThread();
if (Main.bitcoin.wallet().checkAESKey(aesKey)) {
WalletPasswordController.this.aesKey.set(aesKey);
} else {
log.warn("User entered incorrect password");
fadeOut(progressMeter);
fadeIn(widgetGrid);
fadeIn(explanationLabel);
fadeIn(buttonsBox);
informationalAlert("Wrong password",
"Please try entering your password again, carefully checking for typos or spelling errors.");
}
}
};
progressMeter.progressProperty().bind(tasks.progress);
tasks.start();
fadeIn(progressMeter);
fadeOut(widgetGrid);
fadeOut(explanationLabel);
fadeOut(buttonsBox);
}
public void cancelClicked(ActionEvent event) {
overlayUI.done();
}
public ReadOnlyObjectProperty<KeyParameter> aesKeyProperty() {
return aesKey;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static final String TAG = WalletPasswordController.class.getName() + ".target-time";
// Writes the given time to the wallet as a tag so we can find it again in this class.
public static void setTargetTime(Duration targetTime) {
ByteString bytes = ByteString.copyFrom(Longs.toByteArray(targetTime.toMillis()));
Main.bitcoin.wallet().setTag(TAG, bytes);
}
// Reads target time or throws if not set yet (should never happen).
public static Duration getTargetTime() throws IllegalArgumentException {
return Duration.ofMillis(Longs.fromByteArray(Main.bitcoin.wallet().getTag(TAG).toByteArray()));
}
}

View File

@ -0,0 +1,110 @@
package wallettemplate;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import wallettemplate.utils.KeyDerivationTasks;
import java.time.Duration;
import static wallettemplate.utils.GuiUtils.*;
public class WalletSetPasswordController {
private static final Logger log = LoggerFactory.getLogger(WalletSetPasswordController.class);
public PasswordField pass1, pass2;
public ImageView padlockImage;
public ProgressIndicator progressMeter;
public GridPane widgetGrid;
public Button closeButton;
public Label explanationLabel;
public Main.OverlayUI overlayUI;
public void initialize() {
padlockImage.setOpacity(0);
progressMeter.setOpacity(0);
}
public void setPasswordClicked(ActionEvent event) {
if (!pass1.getText().equals(pass2.getText())) {
informationalAlert("Passwords do not match", "Try re-typing your chosen passwords.");
return;
}
String password = pass1.getText();
// This is kind of arbitrary and we could do much more to help people pick strong passwords.
if (password.length() < 4) {
informationalAlert("Password too short", "You need to pick a password at least five characters or longer.");
return;
}
fadeIn(progressMeter);
fadeIn(padlockImage);
fadeOut(widgetGrid);
fadeOut(explanationLabel);
fadeOut(closeButton);
// Figure out how fast this computer can scrypt. We do it on the UI thread because the delay should be small
// and so we don't really care about blocking here.
IdealPasswordParameters params = new IdealPasswordParameters(password);
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(params.realIterations);
// Write the target time to the wallet so we can make the progress bar work when entering the password.
WalletPasswordController.setTargetTime(params.realTargetTime);
// Deriving the actual key runs on a background thread.
KeyDerivationTasks tasks = new KeyDerivationTasks(scrypt, password, params.realTargetTime) {
@Override
protected void onFinish(KeyParameter aesKey) {
// The actual encryption part doesn't take very long as most private keys are derived on demand.
Main.bitcoin.wallet().encrypt(scrypt, aesKey);
fadeIn(explanationLabel);
fadeIn(widgetGrid);
fadeIn(closeButton);
fadeOut(progressMeter);
fadeOut(padlockImage);
}
};
progressMeter.progressProperty().bind(tasks.progress);
tasks.start();
}
public void closeClicked(ActionEvent event) {
overlayUI.done();
}
private static class IdealPasswordParameters {
public final int realIterations;
public final Duration realTargetTime;
public IdealPasswordParameters(String password) {
final int targetTimeMsec = 2000;
int iterations = 16384;
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(iterations);
long now = System.currentTimeMillis();
scrypt.deriveKey(password);
long time = System.currentTimeMillis() - now;
log.info("Initial iterations took {} msec", time);
// N can only be a power of two, so we keep shifting both iterations and doubling time taken
// until we are in sorta the right general area.
while (time < targetTimeMsec) {
iterations <<= 1;
time *= 2;
}
realIterations = iterations;
// Fudge it by +10% to ensure our progress meter is always a bit behind the real encryption. Plus
// without this it seems the real scrypting always takes a bit longer than we estimated for some reason.
realTargetTime = Duration.ofMillis((long) (time * 1.1));
}
}
}

View File

@ -14,14 +14,19 @@ import javafx.scene.control.DatePicker;
import javafx.scene.control.TextArea; import javafx.scene.control.TextArea;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import wallettemplate.utils.TextFieldValidator; import wallettemplate.utils.TextFieldValidator;
import javax.annotation.Nullable;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
import static javafx.beans.binding.Bindings.*; import static javafx.beans.binding.Bindings.*;
import static wallettemplate.utils.GuiUtils.checkGuiThread;
import static wallettemplate.utils.GuiUtils.informationalAlert; import static wallettemplate.utils.GuiUtils.informationalAlert;
import static wallettemplate.utils.WTUtils.didThrow; import static wallettemplate.utils.WTUtils.didThrow;
import static wallettemplate.utils.WTUtils.unchecked; import static wallettemplate.utils.WTUtils.unchecked;
@ -29,15 +34,31 @@ import static wallettemplate.utils.WTUtils.unchecked;
public class WalletSettingsController { public class WalletSettingsController {
private static final Logger log = LoggerFactory.getLogger(WalletSettingsController.class); private static final Logger log = LoggerFactory.getLogger(WalletSettingsController.class);
@FXML Button passwordButton;
@FXML DatePicker datePicker; @FXML DatePicker datePicker;
@FXML TextArea wordsArea; @FXML TextArea wordsArea;
@FXML Button restoreButton; @FXML Button restoreButton;
public Main.OverlayUI overlayUi; public Main.OverlayUI overlayUI;
// Called by FXMLLoader private KeyParameter aesKey;
public void initialize() {
// Note: NOT called by FXMLLoader!
public void initialize(@Nullable KeyParameter aesKey) {
DeterministicSeed seed = Main.bitcoin.wallet().getKeyChainSeed(); DeterministicSeed seed = Main.bitcoin.wallet().getKeyChainSeed();
if (aesKey == null) {
if (seed.isEncrypted()) {
log.info("Wallet is encrypted, requesting password first.");
// Delay execution of this until after we've finished initialising this screen.
Platform.runLater(() -> askForPasswordAndRetry());
return;
}
} else {
this.aesKey = aesKey;
seed = seed.decrypt(checkNotNull(Main.bitcoin.wallet().getKeyCrypter()), "", aesKey);
// Now we can display the wallet seed as appropriate.
passwordButton.setText("Remove password");
}
// Set the date picker to show the birthday of this wallet. // Set the date picker to show the birthday of this wallet.
Instant creationTime = Instant.ofEpochSecond(seed.getCreationTimeSeconds()); Instant creationTime = Instant.ofEpochSecond(seed.getCreationTimeSeconds());
@ -45,7 +66,9 @@ public class WalletSettingsController {
datePicker.setValue(origDate); datePicker.setValue(origDate);
// Set the mnemonic seed words. // Set the mnemonic seed words.
String origWords = Joiner.on(" ").join(seed.getMnemonicCode()); final List<String> mnemonicCode = seed.getMnemonicCode();
checkNotNull(mnemonicCode); // Already checked for encryption.
String origWords = Joiner.on(" ").join(mnemonicCode);
wordsArea.setText(origWords); wordsArea.setText(origWords);
// Validate words as they are being typed. // Validate words as they are being typed.
@ -92,8 +115,19 @@ public class WalletSettingsController {
}); });
} }
private void askForPasswordAndRetry() {
Main.OverlayUI<WalletPasswordController> pwd = Main.instance.overlayUI("wallet_password.fxml");
pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
// We only get here if the user found the right password. If they don't or they cancel, we end up back on
// the main UI screen.
checkGuiThread();
Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("wallet_settings.fxml");
screen.controller.initialize(cur);
});
}
public void closeClicked(ActionEvent event) { public void closeClicked(ActionEvent event) {
overlayUi.done(); overlayUI.done();
} }
public void restoreClicked(ActionEvent event) { public void restoreClicked(ActionEvent event) {
@ -106,10 +140,16 @@ public class WalletSettingsController {
return; return;
} }
if (aesKey != null) {
// This is weak. We should encrypt the new seed here.
informationalAlert("Wallet is encrypted",
"After restore, the wallet will no longer be encrypted and you must set a new password.");
}
log.info("Attempting wallet restore using seed '{}' from date {}", wordsArea.getText(), datePicker.getValue()); log.info("Attempting wallet restore using seed '{}' from date {}", wordsArea.getText(), datePicker.getValue());
informationalAlert("Wallet restore in progress", informationalAlert("Wallet restore in progress",
"Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets."); "Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets.");
overlayUi.done(); overlayUI.done();
Main.instance.controller.restoreFromSeedAnimation(); Main.instance.controller.restoreFromSeedAnimation();
long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC); long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC);
@ -124,4 +164,15 @@ public class WalletSettingsController {
}, Platform::runLater); }, Platform::runLater);
Main.bitcoin.stopAsync(); Main.bitcoin.stopAsync();
} }
public void passwordButtonClicked(ActionEvent event) {
if (aesKey == null) {
Main.instance.overlayUI("wallet_set_password.fxml");
} else {
Main.bitcoin.wallet().decrypt(aesKey);
informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings.");
passwordButton.setText("Set password");
}
}
} }

View File

@ -11,11 +11,14 @@ import javafx.scene.layout.Pane;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration; import javafx.util.Duration;
import wallettemplate.Controller;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static wallettemplate.utils.WTUtils.unchecked;
public class GuiUtils { public class GuiUtils {
public static void runAlert(BiConsumer<Stage, AlertWindowController> setup) { public static void runAlert(BiConsumer<Stage, AlertWindowController> setup) {
@ -66,13 +69,21 @@ public class GuiUtils {
Platform.runLater(r); Platform.runLater(r);
} }
private static final int UI_ANIMATION_TIME_MSEC = 350; private static final int UI_ANIMATION_TIME_MSEC = 600;
public static void fadeIn(Node ui) { public static Animation fadeIn(Node ui) {
return fadeIn(ui, 0);
}
public static Animation fadeIn(Node ui, int delayMillis) {
ui.setCache(true);
FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui); FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui);
ft.setFromValue(0.0); ft.setFromValue(0.0);
ft.setToValue(1.0); ft.setToValue(1.0);
ft.setOnFinished(ev -> ui.setCache(false));
ft.setDelay(Duration.millis(delayMillis));
ft.play(); ft.play();
return ft;
} }
public static Animation fadeOut(Node ui) { public static Animation fadeOut(Node ui) {
@ -83,12 +94,22 @@ public class GuiUtils {
return ft; return ft;
} }
public static Animation fadeOutAndRemove(Node ui, Pane parentPane) { public static Animation fadeOutAndRemove(Pane parentPane, Node... nodes) {
Animation animation = fadeOut(ui); Animation animation = fadeOut(nodes[0]);
animation.setOnFinished(actionEvent -> parentPane.getChildren().remove(ui)); animation.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes));
return animation; return animation;
} }
public static Animation fadeOutAndRemove(Duration duration, Pane parentPane, Node... nodes) {
nodes[0].setCache(true);
FadeTransition ft = new FadeTransition(duration, nodes[0]);
ft.setFromValue(nodes[0].getOpacity());
ft.setToValue(0.0);
ft.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes));
ft.play();
return ft;
}
public static void blurOut(Node node) { public static void blurOut(Node node) {
GaussianBlur blur = new GaussianBlur(0.0); GaussianBlur blur = new GaussianBlur(0.0);
node.setEffect(blur); node.setEffect(blur);
@ -109,6 +130,40 @@ public class GuiUtils {
timeline.play(); timeline.play();
} }
public static ScaleTransition zoomIn(Node node) {
return zoomIn(node, 0);
}
public static ScaleTransition zoomIn(Node node, int delayMillis) {
return scaleFromTo(node, 0.95, 1.0, delayMillis);
}
public static ScaleTransition explodeOut(Node node) {
return scaleFromTo(node, 1.0, 1.05, 0);
}
private static ScaleTransition scaleFromTo(Node node, double from, double to, int delayMillis) {
ScaleTransition scale = new ScaleTransition(Duration.millis(UI_ANIMATION_TIME_MSEC / 2), node);
scale.setFromX(from);
scale.setFromY(from);
scale.setToX(to);
scale.setToY(to);
scale.setDelay(Duration.millis(delayMillis));
scale.play();
return scale;
}
/**
* A useful helper for development purposes. Used as a switch for loading files from local disk, allowing live
* editing whilst the app runs without rebuilds.
*/
public static URL getResource(String name) {
if (false)
return unchecked(() -> new URL("file:///your/path/here/src/main/wallettemplate/" + name));
else
return Controller.class.getResource(name);
}
public static void checkGuiThread() { public static void checkGuiThread() {
checkState(Platform.isFxApplicationThread()); checkState(Platform.isFxApplicationThread());
} }

View File

@ -0,0 +1,80 @@
package wallettemplate.utils;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.common.util.concurrent.Uninterruptibles;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import static wallettemplate.utils.GuiUtils.checkGuiThread;
/**
* Background tasks for pumping a progress meter and deriving an AES key using scrypt.
*/
public class KeyDerivationTasks {
private static final Logger log = LoggerFactory.getLogger(KeyDerivationTasks.class);
public final Task<KeyParameter> keyDerivationTask;
public final ReadOnlyDoubleProperty progress;
private final Task<Void> progressTask;
public KeyDerivationTasks(KeyCrypterScrypt scrypt, String password, Duration targetTime) {
keyDerivationTask = new Task<KeyParameter>() {
@Override
protected KeyParameter call() throws Exception {
try {
return scrypt.deriveKey(password);
} catch (Throwable e) {
e.printStackTrace();
throw e;
} finally {
log.info("Key derivation done");
}
}
};
// And the fake progress meter ...
progressTask = new Task<Void>() {
private KeyParameter aesKey;
@Override
protected Void call() throws Exception {
long startTime = System.currentTimeMillis();
long curTime;
long targetTimeMillis = targetTime.toMillis();
while ((curTime = System.currentTimeMillis()) < startTime + targetTimeMillis) {
double progress = (curTime - startTime) / (double) targetTimeMillis;
updateProgress(progress, 1.0);
// 60fps would require 16msec sleep here.
Uninterruptibles.sleepUninterruptibly(20, TimeUnit.MILLISECONDS);
}
// Wait for the encryption thread before switching back to main UI.
updateProgress(1.0, 1.0);
aesKey = keyDerivationTask.get();
return null;
}
@Override
protected void succeeded() {
checkGuiThread();
onFinish(aesKey);
}
};
progress = progressTask.progressProperty();
}
public void start() {
new Thread(keyDerivationTask, "Key derivation").start();
new Thread(progressTask, "Progress ticker").start();
}
protected void onFinish(KeyParameter aesKey) {
}
}

View File

@ -13,4 +13,4 @@
.root-pane { .root-pane {
-fx-background-color: white; -fx-background-color: white;
} }

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<StackPane maxHeight="Infinity" maxWidth="Infinity" prefHeight="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletPasswordController">
<children>
<AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane">
<children>
<ImageView fx:id="padlockImage" fitHeight="389.0" fitWidth="389.0" layoutX="14.0" layoutY="-4.0" opacity="0.22" pickOnBounds="true" preserveRatio="true" visible="false" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="197.0">
<image>
<!-- image from wikipedia -->
<Image url="@200px-Padlock.svg.png" />
</image>
</ImageView>
<HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<children>
<Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="password">
<font>
<Font size="30.0" />
</font>
<HBox.margin>
<Insets />
</HBox.margin>
<padding>
<Insets left="20.0" right="20.0" />
</padding>
</Label>
</children>
</HBox>
<ProgressIndicator fx:id="progressMeter" layoutX="218.0" layoutY="146.0" prefHeight="193.0" prefWidth="169.0" progress="0.0" AnchorPane.leftAnchor="218.0" AnchorPane.rightAnchor="213.0" />
<Label fx:id="explanationLabel" layoutX="22.0" layoutY="83.0" prefHeight="52.0" prefWidth="561.0" text="Please enter your wallet password now:" wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
<font>
<Font name="System Bold" size="13.0" />
</font></Label>
<HBox fx:id="buttonsBox" alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="561.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0">
<children>
<Button fx:id="cancelButton" cancelButton="true" mnemonicParsing="false" onAction="#cancelClicked" text="Cancel" />
<Button fx:id="confirmButton" defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#confirmClicked" text="Confirm" HBox.hgrow="ALWAYS" />
</children>
</HBox>
<GridPane fx:id="widgetGrid" layoutX="22.0" layoutY="146.0" prefHeight="114.0" prefWidth="561.0" vgap="10.0" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="273.0" minWidth="10.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="417.0" minWidth="10.0" prefWidth="417.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="Password" />
<PasswordField fx:id="pass1" GridPane.columnIndex="1" />
</children>
</GridPane>
</children>
<effect>
<DropShadow />
</effect>
</AnchorPane>
</children>
</StackPane>

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<StackPane maxHeight="Infinity" maxWidth="Infinity" prefHeight="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletSetPasswordController">
<children>
<AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane">
<children>
<HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<children>
<Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="password">
<font>
<Font size="30.0" />
</font>
<HBox.margin>
<Insets />
</HBox.margin>
<padding>
<Insets left="20.0" right="20.0" />
</padding>
</Label>
</children>
</HBox>
<Label fx:id="explanationLabel" layoutX="22.0" layoutY="83.0" prefHeight="52.0" prefWidth="561.0" text="Setting a password on your wallet makes it safer against viruses and theft. You will need to enter your password whenever money is sent." wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0" />
<HBox alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="561.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0">
<children>
<Button fx:id="closeButton" defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#closeClicked" text="Close" HBox.hgrow="ALWAYS" />
</children>
</HBox>
<ProgressIndicator fx:id="progressMeter" layoutX="250.0" layoutY="133.0" prefHeight="114.0" prefWidth="87.0" progress="0.0" AnchorPane.leftAnchor="250.0" AnchorPane.rightAnchor="250.0" />
<GridPane fx:id="widgetGrid" layoutX="22.0" layoutY="146.0" prefHeight="114.0" prefWidth="561.0" vgap="10.0" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="273.0" minWidth="10.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="417.0" minWidth="10.0" prefWidth="417.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="Enter password" />
<Label text="Repeat password" GridPane.rowIndex="1" />
<PasswordField fx:id="pass1" GridPane.columnIndex="1" />
<PasswordField fx:id="pass2" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Button mnemonicParsing="false" onAction="#setPasswordClicked" text="Set password" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="2" GridPane.valignment="TOP" />
</children>
</GridPane>
<ImageView fx:id="padlockImage" fitHeight="125.0" fitWidth="125.0" layoutX="234.0" layoutY="262.0" pickOnBounds="true" preserveRatio="true" AnchorPane.leftAnchor="240.0" AnchorPane.rightAnchor="240.0">
<image>
<Image url="@200px-Padlock.svg.png" />
</image>
</ImageView>
</children>
<effect>
<DropShadow />
</effect>
</AnchorPane>
</children>
</StackPane>

View File

@ -9,48 +9,48 @@
<?import javafx.scene.paint.*?> <?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?> <?import javafx.scene.text.*?>
<StackPane maxHeight="Infinity" maxWidth="Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletSettingsController"> <StackPane maxHeight="Infinity" maxWidth="Infinity" prefHeight="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletSettingsController">
<children> <children>
<AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane"> <AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane">
<children> <children>
<HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0"> <HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
<children> <children>
<Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="settings"> <Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="settings">
<font> <font>
<Font size="30.0" /> <Font size="30.0" />
</font> </font>
<HBox.margin> <HBox.margin>
<Insets /> <Insets />
</HBox.margin> </HBox.margin>
<padding> <padding>
<Insets left="20.0" right="20.0" /> <Insets left="20.0" right="20.0" />
</padding> </padding>
</Label> </Label>
</children> </children>
</HBox> </HBox>
<TextArea fx:id="wordsArea" layoutX="22.0" layoutY="155.0" prefHeight="127.0" prefWidth="561.0" styleClass="mnemonic-area" text="absorb tornado scrap blush purpose ethics destroy vicious abandon chunk labor inquiry" wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0"> <TextArea fx:id="wordsArea" layoutX="22.0" layoutY="155.0" prefHeight="127.0" prefWidth="561.0" styleClass="mnemonic-area" text="absorb tornado scrap blush purpose ethics destroy vicious abandon chunk labor inquiry" wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
<font> <font>
<Font size="30.0" /> <Font size="30.0" />
</font> </font>
</TextArea> </TextArea>
<Label layoutX="35.0" layoutY="82.0" prefHeight="52.0" prefWidth="530.0" text="These are your wallet words. Write them down along with the creation date, and you can get your money back even if you lose all your wallet backup files. Just type the details back in below to restore!" textAlignment="CENTER" wrapText="true" /> <Label layoutX="22.0" layoutY="83.0" prefHeight="52.0" prefWidth="561.0" text="These are your wallet words. Write them down along with the creation date, and you can get your money back even if you lose all your wallet backup files. Just type the details back in below to restore!" wrapText="true" />
<HBox alignment="CENTER_RIGHT" layoutX="22.0" layoutY="292.0" prefHeight="26.0" prefWidth="561.0" spacing="10.0" AnchorPane.rightAnchor="17.0"> <HBox alignment="CENTER_RIGHT" layoutX="22.0" layoutY="292.0" prefHeight="26.0" prefWidth="561.0" spacing="10.0" AnchorPane.rightAnchor="17.0">
<children> <children>
<Label layoutX="64.0" layoutY="283.0" text="Created on:" /> <Label layoutX="64.0" layoutY="283.0" text="Created on:" />
<DatePicker fx:id="datePicker" /> <DatePicker fx:id="datePicker" />
</children> </children>
</HBox> </HBox>
<HBox alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="311.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0"> <HBox alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="561.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0">
<children> <children>
<Button maxWidth="80.0" mnemonicParsing="false" text="Backup" HBox.hgrow="ALWAYS" /> <Button fx:id="passwordButton" mnemonicParsing="false" onAction="#passwordButtonClicked" text="Set password" />
<Button fx:id="restoreButton" maxWidth="80.0" mnemonicParsing="false" onAction="#restoreClicked" text="Restore" HBox.hgrow="ALWAYS" /> <Button fx:id="restoreButton" mnemonicParsing="false" onAction="#restoreClicked" text="Restore from words" HBox.hgrow="ALWAYS" />
<Button defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#closeClicked" text="Close" HBox.hgrow="ALWAYS" /> <Button defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#closeClicked" text="Close" HBox.hgrow="ALWAYS" />
</children> </children>
</HBox> </HBox>
</children> </children>
<effect> <effect>
<DropShadow /> <DropShadow />
</effect> </effect>
</AnchorPane> </AnchorPane>
</children> </children>
</StackPane> </StackPane>