handle password key derivation in separate tasks

This commit is contained in:
Craig Raw 2020-05-19 16:42:46 +02:00
parent b52a7137c3
commit 2acc922b06
7 changed files with 285 additions and 112 deletions

2
drongo

@ -1 +1 @@
Subproject commit d2bd335e76a6a18634d033b49bf2d36c2494d9cf
Subproject commit 06de1d7e1458cb00cc242025c5e0d536633083a0

View file

@ -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<SecureString> 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<String> 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();
}
}

View file

@ -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<SecureString> 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);
}
}
}

View file

@ -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;
}
}

View file

@ -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<String> 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<WalletAndKey> {
private final Storage storage;
private final SecureString password;
public LoadWalletService(Storage storage, SecureString password) {
this.storage = storage;
this.password = password;
}
@Override
protected Task<WalletAndKey> 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<ECKey> {
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<ECKey> 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<Wallet> {
private final Wallet wallet;
private final SecureString password;
public DecryptWalletService(Wallet wallet, SecureString password) {
this.wallet = wallet;
this.password = password;
}
@Override
protected Task<Wallet> createTask() {
return new Task<>() {
protected Wallet call() throws IOException, StorageException {
try {
wallet.decrypt(password);
return wallet;
} finally {
password.clear();
}
}
};
}

View file

@ -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<ECKey> 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<ECKey> 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<SecureString> 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();
}
}

View file

@ -41,6 +41,6 @@
<TabPane fx:id="tabs" />
</StackPane>
<StatusBar text=""/>
<StatusBar fx:id="statusBar" text=""/>
</children>
</VBox>