diff --git a/drongo b/drongo index d2bd335e..06de1d7e 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit d2bd335e76a6a18634d033b49bf2d36c2494d9cf +Subproject commit 06de1d7e1458cb00cc242025c5e0d536633083a0 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 2aeccd40..e5c5f197 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -11,6 +11,8 @@ import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBTParseException; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.MnemonicException; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; @@ -20,6 +22,10 @@ import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.wallet.WalletController; import com.sparrowwallet.sparrow.wallet.WalletForm; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; @@ -30,6 +36,8 @@ import javafx.scene.input.TransferMode; import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; import javafx.stage.Stage; +import javafx.util.Duration; +import org.controlsfx.control.StatusBar; import java.io.*; import java.net.URL; @@ -52,11 +60,16 @@ public class AppController implements Initializable { @FXML private TabPane tabs; + @FXML + private StatusBar statusBar; + + private Timeline statusTimeline; + public static boolean showTxHexProperty; @Override public void initialize(URL location, ResourceBundle resources) { - + EventManager.get().register(this); } void initializeView() { @@ -234,12 +247,12 @@ public class AppController implements Initializable { File file = fileChooser.showOpenDialog(window); if(file != null) { try { - Wallet wallet; - CharSequence password = null; Storage storage = new Storage(file); FileType fileType = IOUtils.getFileType(file); if(FileType.JSON.equals(fileType)) { - wallet = storage.loadWallet(); + Wallet wallet = storage.loadWallet(); + Tab tab = addWalletTab(storage, wallet); + tabs.getSelectionModel().select(tab); } else if(FileType.BINARY.equals(fileType)) { WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD); Optional optionalPassword = dlg.showAndWait(); @@ -247,18 +260,79 @@ public class AppController implements Initializable { return; } - password = optionalPassword.get(); - wallet = storage.loadWallet(password); + SecureString password = optionalPassword.get(); + Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password); + loadWalletService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new TimedWorkerEvent("Done")); + Storage.WalletAndKey walletAndKey = loadWalletService.getValue(); + try { + restorePublicKeysFromSeed(walletAndKey.wallet, walletAndKey.key); + Tab tab = addWalletTab(storage, walletAndKey.wallet); + tabs.getSelectionModel().select(tab); + } catch(MnemonicException e) { + showErrorDialog("Error Opening Wallet", e.getMessage()); + } finally { + walletAndKey.key.clear(); + } + }); + loadWalletService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new TimedWorkerEvent("Failed")); + Throwable exception = loadWalletService.getException(); + if(exception instanceof InvalidPasswordException) { + showErrorDialog("Invalid Password", "The wallet password was invalid."); + } else { + showErrorDialog("Error Opening Wallet", exception.getMessage()); + } + }); + loadWalletService.start(); + EventManager.get().post(new TimedWorkerEvent("Decrypting wallet...", 1000)); } else { throw new IOException("Unsupported file type"); } + } catch(Exception e) { + showErrorDialog("Error Opening Wallet", e.getMessage()); + } + } + } - Tab tab = addWalletTab(storage, wallet); - tabs.getSelectionModel().select(tab); - } catch (InvalidPasswordException e) { - showErrorDialog("Invalid Password", "The password was invalid."); - } catch (Exception e) { - showErrorDialog("Error opening wallet", e.getMessage()); + private void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException { + if(wallet.containsSeeds()) { + //Derive xpub and master fingerprint from seed, potentially with passphrase + Wallet copy = wallet.copy(); + for(Keystore copyKeystore : copy.getKeystores()) { + if(copyKeystore.hasSeed()) { + if(copyKeystore.getSeed().needsPassphrase()) { + KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(copyKeystore); + Optional optionalPassphrase = passphraseDialog.showAndWait(); + if(optionalPassphrase.isPresent()) { + copyKeystore.getSeed().setPassphrase(optionalPassphrase.get()); + } else { + return; + } + } else { + copyKeystore.getSeed().setPassphrase(""); + } + } + } + + if(wallet.isEncrypted()) { + if(key == null) { + throw new IllegalStateException("Wallet was not encrypted, but seed is"); + } + + copy.decrypt(key); + } + + for(int i = 0; i < wallet.getKeystores().size(); i++) { + Keystore keystore = wallet.getKeystores().get(i); + if(keystore.hasSeed()) { + Keystore copyKeystore = copy.getKeystores().get(i); + Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation()); + keystore.setKeyDerivation(derivedKeystore.getKeyDerivation()); + keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey()); + keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase()); + copyKeystore.getSeed().clear(); + } } } } @@ -418,7 +492,6 @@ public class AppController implements Initializable { @Subscribe public void tabSelected(TabSelectedEvent event) { Tab selectedTab = event.getTab(); - String tabType = (String)selectedTab.getUserData(); String tabName = selectedTab.getText(); if(tabs.getScene() != null) { @@ -426,4 +499,28 @@ public class AppController implements Initializable { tabStage.setTitle("Sparrow - " + tabName); } } + + @Subscribe + public void timedWorker(TimedWorkerEvent event) { + if(statusTimeline != null && statusTimeline.getStatus() == Animation.Status.RUNNING) { + if(event.getTimeMills() == 0) { + statusTimeline.stop(); + statusBar.setText(""); + statusBar.setProgress(0); + } + + return; + } + + statusBar.setText(event.getStatus()); + statusTimeline = new Timeline( + new KeyFrame(Duration.ZERO, new KeyValue(statusBar.progressProperty(), 0)), + new KeyFrame(Duration.millis(event.getTimeMills()), e -> { + statusBar.setText(""); + statusBar.setProgress(0); + }, new KeyValue(statusBar.progressProperty(), 1)) + ); + statusTimeline.setCycleCount(1); + statusTimeline.play(); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java index 221a2cfc..eae29c9a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java @@ -3,7 +3,9 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.TimedWorkerEvent; import com.sparrowwallet.sparrow.event.WalletExportEvent; +import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.WalletExport; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -55,22 +57,31 @@ public class FileWalletExportPane extends TitledDescriptionPane { WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD); Optional password = dlg.showAndWait(); if(password.isPresent()) { - copy.decrypt(password.get()); - } else { - return; + Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get()); + decryptWalletService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new TimedWorkerEvent("Done")); + Wallet decryptedWallet = decryptWalletService.getValue(); + try { + OutputStream outputStream = new FileOutputStream(file); + exporter.exportWallet(decryptedWallet, outputStream); + EventManager.get().post(new WalletExportEvent(decryptedWallet)); + } catch(Exception e) { + String errorMessage = e.getMessage(); + if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { + errorMessage = e.getCause().getMessage(); + } + setError("Export Error", errorMessage); + } finally { + decryptedWallet.clearPrivate(); + } + }); + decryptWalletService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new TimedWorkerEvent("Failed")); + setError("Export Error", decryptWalletService.getException().getMessage()); + }); + decryptWalletService.start(); + EventManager.get().post(new TimedWorkerEvent("Decrypting wallet...", 1000)); } } - - try { - OutputStream outputStream = new FileOutputStream(file); - exporter.exportWallet(copy, outputStream); - EventManager.get().post(new WalletExportEvent(copy)); - } catch(Exception e) { - String errorMessage = e.getMessage(); - if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { - errorMessage = e.getCause().getMessage(); - } - setError("Export Error", errorMessage); - } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TimedWorkerEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TimedWorkerEvent.java new file mode 100644 index 00000000..1cf38828 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/TimedWorkerEvent.java @@ -0,0 +1,24 @@ +package com.sparrowwallet.sparrow.event; + +public class TimedWorkerEvent { + private final String status; + private final int timeMills; + + public TimedWorkerEvent(String status) { + this.status = status; + this.timeMills = 0; + } + + public TimedWorkerEvent(String status, int timeMills) { + this.status = status; + this.timeMills = timeMills; + } + + public String getStatus() { + return status; + } + + public int getTimeMills() { + return timeMills; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 980eb3ef..973b1c32 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -2,12 +2,12 @@ package com.sparrowwallet.sparrow.io; import com.google.gson.*; import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.crypto.*; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.MnemonicException; import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog; import javafx.concurrent.Service; import javafx.concurrent.Task; @@ -18,7 +18,6 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; -import java.util.Optional; import java.util.zip.*; import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS; @@ -68,12 +67,10 @@ public class Storage { Wallet wallet = gson.fromJson(reader, Wallet.class); reader.close(); - restorePublicKeysFromSeed(wallet, null); - return wallet; } - public Wallet loadWallet(CharSequence password) throws IOException, MnemonicException, StorageException { + public WalletAndKey loadWallet(CharSequence password) throws IOException, MnemonicException, StorageException { InputStream fileStream = new FileInputStream(walletFile); ECKey encryptionKey = getEncryptionKey(password, fileStream); @@ -83,52 +80,9 @@ public class Storage { reader.close(); Key key = new Key(encryptionKey.getPrivKeyBytes(), keyDeriver.getSalt(), EncryptionType.Deriver.ARGON2); - restorePublicKeysFromSeed(wallet, key); encryptionPubKey = ECKey.fromPublicOnly(encryptionKey); - return wallet; - } - - private void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException { - if(wallet.containsSeeds()) { - //Derive xpub and master fingerprint from seed, potentially with passphrase - Wallet copy = wallet.copy(); - for(Keystore copyKeystore : copy.getKeystores()) { - if(copyKeystore.hasSeed()) { - if(copyKeystore.getSeed().needsPassphrase()) { - KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(copyKeystore); - Optional optionalPassphrase = passphraseDialog.showAndWait(); - if(optionalPassphrase.isPresent()) { - copyKeystore.getSeed().setPassphrase(optionalPassphrase.get()); - } else { - return; - } - } else { - copyKeystore.getSeed().setPassphrase(""); - } - } - } - - if(wallet.isEncrypted()) { - if(key == null) { - throw new IllegalStateException("Wallet was not encrypted, but seed is"); - } - - copy.decrypt(key); - } - - for(int i = 0; i < wallet.getKeystores().size(); i++) { - Keystore keystore = wallet.getKeystores().get(i); - if(keystore.hasSeed()) { - Keystore copyKeystore = copy.getKeystores().get(i); - Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation()); - keystore.setKeyDerivation(derivedKeystore.getKeyDerivation()); - keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey()); - keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase()); - copyKeystore.getSeed().clear(); - } - } - } + return new WalletAndKey(wallet, key); } public void storeWallet(Wallet wallet) throws IOException { @@ -316,11 +270,44 @@ public class Storage { } } + public static class WalletAndKey { + public final Wallet wallet; + public final Key key; + + public WalletAndKey(Wallet wallet, Key key) { + this.wallet = wallet; + this.key = key; + } + } + + public static class LoadWalletService extends Service { + private final Storage storage; + private final SecureString password; + + public LoadWalletService(Storage storage, SecureString password) { + this.storage = storage; + this.password = password; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected WalletAndKey call() throws IOException, StorageException, MnemonicException { + try { + return storage.loadWallet(password); + } finally { + password.clear(); + } + } + }; + } + } + public static class KeyDerivationService extends Service { private final Storage storage; - private final String password; + private final SecureString password; - public KeyDerivationService(Storage storage, String password) { + public KeyDerivationService(Storage storage, SecureString password) { this.storage = storage; this.password = password; } @@ -329,7 +316,35 @@ public class Storage { protected Task createTask() { return new Task<>() { protected ECKey call() throws IOException, StorageException { - return storage.getEncryptionKey(password); + try { + return storage.getEncryptionKey(password); + } finally { + password.clear(); + } + } + }; + } + } + + public static class DecryptWalletService extends Service { + private final Wallet wallet; + private final SecureString password; + + public DecryptWalletService(Wallet wallet, SecureString password) { + this.wallet = wallet; + this.password = password; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected Wallet call() throws IOException, StorageException { + try { + wallet.decrypt(password); + return wallet; + } finally { + password.clear(); + } } }; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 533dd6f4..eedb0710 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -15,6 +15,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.CopyableLabel; import com.sparrowwallet.sparrow.control.WalletPasswordDialog; import com.sparrowwallet.sparrow.event.SettingsChangedEvent; +import com.sparrowwallet.sparrow.event.TimedWorkerEvent; import com.sparrowwallet.sparrow.event.WalletChangedEvent; import com.sparrowwallet.sparrow.io.Storage; import javafx.beans.property.SimpleIntegerProperty; @@ -158,18 +159,9 @@ public class SettingsController extends WalletFormController implements Initiali }); apply.setOnAction(event -> { - try { - Optional optionalPubKey = requestEncryption(walletForm.getStorage().getEncryptionPubKey()); - if(optionalPubKey.isPresent()) { - walletForm.getStorage().setEncryptionPubKey(optionalPubKey.get()); - walletForm.save(); - revert.setDisable(true); - apply.setDisable(true); - EventManager.get().post(new WalletChangedEvent(walletForm.getWallet(), walletForm.getWalletFile())); - } - } catch (IOException e) { - AppController.showErrorDialog("Error saving file", e.getMessage()); - } + revert.setDisable(true); + apply.setDisable(true); + saveWallet(); }); setFieldsFromWallet(walletForm.getWallet()); @@ -256,7 +248,9 @@ public class SettingsController extends WalletFormController implements Initiali } } - private Optional requestEncryption(ECKey existingPubKey) { + private void saveWallet() { + ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey(); + WalletPasswordDialog.PasswordRequirement requirement; if(existingPubKey == null) { requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_NEW; @@ -270,26 +264,58 @@ public class SettingsController extends WalletFormController implements Initiali Optional password = dlg.showAndWait(); if(password.isPresent()) { if(password.get().length() == 0) { - return Optional.of(Storage.NO_PASSWORD_KEY); - } - - try { - ECKey encryptionFullKey = walletForm.getStorage().getEncryptionKey(password.get()); - ECKey encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); - - if(existingPubKey != null && !Storage.NO_PASSWORD_KEY.equals(existingPubKey) && !existingPubKey.equals(encryptionPubKey)) { - AppController.showErrorDialog("Incorrect Password", "The password was incorrect."); - return Optional.empty(); + try { + walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY); + walletForm.save(); + EventManager.get().post(new WalletChangedEvent(walletForm.getWallet(), walletForm.getWalletFile())); + } catch (IOException e) { + AppController.showErrorDialog("Error saving wallet", e.getMessage()); + revert.setDisable(false); + apply.setDisable(false); } + } else { + Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get()); + keyDerivationService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new TimedWorkerEvent("Done")); + ECKey encryptionFullKey = keyDerivationService.getValue(); + Key key = null; - Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); - walletForm.getWallet().encrypt(key); - return Optional.of(encryptionPubKey); - } catch (Exception e) { - AppController.showErrorDialog("Wallet File Invalid", e.getMessage()); + try { + ECKey encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); + + if(existingPubKey != null && !Storage.NO_PASSWORD_KEY.equals(existingPubKey) && !existingPubKey.equals(encryptionPubKey)) { + AppController.showErrorDialog("Incorrect Password", "The password was incorrect."); + revert.setDisable(false); + apply.setDisable(false); + return; + } + + key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); + walletForm.getWallet().encrypt(key); + + walletForm.getStorage().setEncryptionPubKey(encryptionPubKey); + walletForm.save(); + EventManager.get().post(new WalletChangedEvent(walletForm.getWallet(), walletForm.getWalletFile())); + } catch (Exception e) { + AppController.showErrorDialog("Error saving wallet", e.getMessage()); + revert.setDisable(false); + apply.setDisable(false); + } finally { + encryptionFullKey.clear(); + if(key != null) { + key.clear(); + } + } + }); + keyDerivationService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new TimedWorkerEvent("Failed")); + AppController.showErrorDialog("Error saving wallet", keyDerivationService.getException().getMessage()); + revert.setDisable(false); + apply.setDisable(false); + }); + keyDerivationService.start(); + EventManager.get().post(new TimedWorkerEvent("Encrypting wallet...", 1000)); } } - - return Optional.empty(); } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index a0e41d88..0803d490 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -41,6 +41,6 @@ - +