wallet encryption with argon2 key derivation

This commit is contained in:
Craig Raw 2020-05-18 15:50:46 +02:00
parent ae01fe9ee6
commit 68aeb5946a
11 changed files with 270 additions and 160 deletions

2
drongo

@ -1 +1 @@
Subproject commit e20501d95422bb4ef76002cb7a42c46b856143d9 Subproject commit 8ffd22500754b77e420e2a3f887864e89a47e906

View file

@ -217,9 +217,10 @@ public class AppController implements Initializable {
WalletNameDialog dlg = new WalletNameDialog(); WalletNameDialog dlg = new WalletNameDialog();
Optional<String> walletName = dlg.showAndWait(); Optional<String> walletName = dlg.showAndWait();
if(walletName.isPresent()) { if(walletName.isPresent()) {
File walletFile = Storage.getStorage().getWalletFile(walletName.get()); File walletFile = Storage.getWalletFile(walletName.get());
Storage storage = new Storage(walletFile);
Wallet wallet = new Wallet(walletName.get(), PolicyType.SINGLE, ScriptType.P2WPKH); Wallet wallet = new Wallet(walletName.get(), PolicyType.SINGLE, ScriptType.P2WPKH);
Tab tab = addWalletTab(walletFile, null, wallet); Tab tab = addWalletTab(storage, wallet);
tabs.getSelectionModel().select(tab); tabs.getSelectionModel().select(tab);
} }
} }
@ -228,17 +229,17 @@ public class AppController implements Initializable {
Stage window = new Stage(); Stage window = new Stage();
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Wallet"); fileChooser.setTitle("Open Wallet");
fileChooser.setInitialDirectory(Storage.getStorage().getWalletsDir()); fileChooser.setInitialDirectory(Storage.getWalletsDir());
File file = fileChooser.showOpenDialog(window); File file = fileChooser.showOpenDialog(window);
if(file != null) { if(file != null) {
try { try {
Wallet wallet; Wallet wallet;
String password = null; String password = null;
ECKey encryptionPubKey = WalletForm.NO_PASSWORD_KEY; Storage storage = new Storage(file);
FileType fileType = IOUtils.getFileType(file); FileType fileType = IOUtils.getFileType(file);
if(FileType.JSON.equals(fileType)) { if(FileType.JSON.equals(fileType)) {
wallet = Storage.getStorage().loadWallet(file); wallet = storage.loadWallet();
} else if(FileType.BINARY.equals(fileType)) { } else if(FileType.BINARY.equals(fileType)) {
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD); WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<String> optionalPassword = dlg.showAndWait(); Optional<String> optionalPassword = dlg.showAndWait();
@ -247,53 +248,12 @@ public class AppController implements Initializable {
} }
password = optionalPassword.get(); password = optionalPassword.get();
ECKey encryptionFullKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey(password); wallet = storage.loadWallet(password);
wallet = Storage.getStorage().loadWallet(file, encryptionFullKey);
encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey);
} else { } else {
throw new IOException("Unsupported file type"); throw new IOException("Unsupported file type");
} }
if(wallet.containsSeeds()) { Tab tab = addWalletTab(storage, wallet);
//Derive xpub and master fingerprint from seed, potentially with passphrase
Wallet copy = wallet.copy();
if(wallet.isEncrypted()) {
if(password == null) {
throw new IllegalStateException("Wallet seeds are encrypted but wallet is not");
}
copy.decrypt(password);
}
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("");
}
}
}
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());
}
}
}
Tab tab = addWalletTab(file, encryptionPubKey, wallet);
tabs.getSelectionModel().select(tab); tabs.getSelectionModel().select(tab);
} catch (InvalidPasswordException e) { } catch (InvalidPasswordException e) {
showErrorDialog("Invalid Password", "The password was invalid."); showErrorDialog("Invalid Password", "The password was invalid.");
@ -308,8 +268,9 @@ public class AppController implements Initializable {
Optional<Wallet> optionalWallet = dlg.showAndWait(); Optional<Wallet> optionalWallet = dlg.showAndWait();
if(optionalWallet.isPresent()) { if(optionalWallet.isPresent()) {
Wallet wallet = optionalWallet.get(); Wallet wallet = optionalWallet.get();
File walletFile = Storage.getStorage().getWalletFile(wallet.getName()); File walletFile = Storage.getWalletFile(wallet.getName());
Tab tab = addWalletTab(walletFile, null, wallet); Storage storage = new Storage(walletFile);
Tab tab = addWalletTab(storage, wallet);
tabs.getSelectionModel().select(tab); tabs.getSelectionModel().select(tab);
} }
} }
@ -327,21 +288,21 @@ public class AppController implements Initializable {
} }
} }
public Tab addWalletTab(File walletFile, ECKey encryptionPubKey, Wallet wallet) { public Tab addWalletTab(Storage storage, Wallet wallet) {
try { try {
String name = walletFile.getName(); String name = storage.getWalletFile().getName();
if(name.endsWith(".json")) { if(name.endsWith(".json")) {
name = name.substring(0, name.lastIndexOf('.')); name = name.substring(0, name.lastIndexOf('.'));
} }
Tab tab = new Tab(name); Tab tab = new Tab(name);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, wallet, walletFile); TabData tabData = new WalletTabData(TabData.TabType.WALLET, wallet, storage);
tab.setUserData(tabData); tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab)); tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true); tab.setClosable(true);
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml")); FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
tab.setContent(walletLoader.load()); tab.setContent(walletLoader.load());
WalletController controller = walletLoader.getController(); WalletController controller = walletLoader.getController();
WalletForm walletForm = new WalletForm(walletFile, encryptionPubKey, wallet); WalletForm walletForm = new WalletForm(storage, wallet);
controller.setWalletForm(walletForm); controller.setWalletForm(walletForm);
tabs.getTabs().add(tab); tabs.getTabs().add(tab);

View file

@ -3,17 +3,16 @@ package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.event.WalletChangedEvent; import com.sparrowwallet.sparrow.event.WalletChangedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import java.io.File;
public class WalletTabData extends TabData { public class WalletTabData extends TabData {
private Wallet wallet; private Wallet wallet;
private final File walletFile; private final Storage storage;
public WalletTabData(TabType type, Wallet wallet, File walletFile) { public WalletTabData(TabType type, Wallet wallet, Storage storage) {
super(type); super(type);
this.wallet = wallet; this.wallet = wallet;
this.walletFile = walletFile; this.storage = storage;
EventManager.get().register(this); EventManager.get().register(this);
} }
@ -22,13 +21,13 @@ public class WalletTabData extends TabData {
return wallet; return wallet;
} }
public File getWalletFile() { public Storage getStorage() {
return walletFile; return storage;
} }
@Subscribe @Subscribe
public void walletChanged(WalletChangedEvent event) { public void walletChanged(WalletChangedEvent event) {
if(event.getWalletFile().equals(walletFile)) { if(event.getWalletFile().equals(storage.getWalletFile())) {
wallet = event.getWallet(); wallet = event.getWallet();
} }
} }

View file

@ -43,7 +43,7 @@ public class WalletNameDialog extends Dialog<String> {
Platform.runLater( () -> { Platform.runLater( () -> {
validationSupport.registerValidator(name, Validator.combine( validationSupport.registerValidator(name, Validator.combine(
Validator.createEmptyValidator("Wallet name is required"), Validator.createEmptyValidator("Wallet name is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.getStorage().getWalletFile(newValue).exists()) (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.getWalletFile(newValue).exists())
)); ));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
}); });
@ -52,7 +52,7 @@ public class WalletNameDialog extends Dialog<String> {
dialogPane.getButtonTypes().addAll(okButtonType); dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType); Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> BooleanBinding isInvalid = Bindings.createBooleanBinding(() ->
name.getText().length() == 0 || Storage.getStorage().getWalletFile(name.getText()).exists(), name.textProperty()); name.getText().length() == 0 || Storage.getWalletFile(name.getText()).exists(), name.textProperty());
okButton.disableProperty().bind(isInvalid); okButton.disableProperty().bind(isInvalid);
name.setPromptText("Wallet Name"); name.setPromptText("Wallet Name");

View file

@ -3,33 +3,45 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*; import com.google.gson.*;
import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
import java.io.*; import java.io.*;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; 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 java.util.zip.*;
import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS;
public class Storage { public class Storage {
public static final ECKey NO_PASSWORD_KEY = ECKey.fromPublicOnly(ECKey.fromPrivate(Utils.hexToBytes("885e5a09708a167ea356a252387aa7c4893d138d632e296df8fbf5c12798bd28")));
public static final String SPARROW_DIR = ".sparrow"; public static final String SPARROW_DIR = ".sparrow";
public static final String WALLETS_DIR = "wallets"; public static final String WALLETS_DIR = "wallets";
public static final String HEADER_MAGIC_1 = "SPRW1";
private static final int BINARY_HEADER_LENGTH = 28;
private static Storage SINGLETON; private File walletFile;
private final Gson gson; private final Gson gson;
private AsymmetricKeyDeriver keyDeriver;
private ECKey encryptionPubKey;
private Storage() { public Storage(File walletFile) {
gson = getGson(); this.walletFile = walletFile;
this.gson = getGson();
this.encryptionPubKey = NO_PASSWORD_KEY;
} }
public static Storage getStorage() { public File getWalletFile() {
if(SINGLETON == null) { return walletFile;
SINGLETON = new Storage();
}
return SINGLETON;
} }
public static Gson getGson() { public static Gson getGson() {
@ -49,73 +61,200 @@ public class Storage {
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create(); return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
} }
public Wallet loadWallet(File file) throws IOException { public Wallet loadWallet() throws IOException, MnemonicException {
Reader reader = new FileReader(file); Reader reader = new FileReader(walletFile);
Wallet wallet = gson.fromJson(reader, Wallet.class); Wallet wallet = gson.fromJson(reader, Wallet.class);
reader.close(); reader.close();
restorePublicKeysFromSeed(wallet, null);
return wallet; return wallet;
} }
public Wallet loadWallet(File file, ECKey encryptionKey) throws IOException { public Wallet loadWallet(String password) throws IOException, MnemonicException, StorageException {
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(new FileInputStream(file), encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8); InputStream fileStream = new FileInputStream(walletFile);
ECKey encryptionKey = getEncryptionKey(password, fileStream);
InputStream inputStream = new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic()));
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
Wallet wallet = gson.fromJson(reader, Wallet.class); Wallet wallet = gson.fromJson(reader, Wallet.class);
reader.close(); reader.close();
Key key = new Key(encryptionKey.getPrivKeyBytes(), keyDeriver.getSalt(), EncryptionType.Deriver.ARGON2);
restorePublicKeysFromSeed(wallet, key);
encryptionPubKey = ECKey.fromPublicOnly(encryptionKey);
return wallet; return wallet;
} }
public void storeWallet(File file, Wallet wallet) throws IOException { private void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException {
File parent = file.getParentFile(); if(wallet.containsSeeds()) {
//Derive xpub and master fingerprint from seed, potentially with passphrase
Wallet copy = wallet.copy();
if(wallet.isEncrypted()) {
if(key == null) {
throw new IllegalStateException("Wallet was not encrypted, but seed is");
}
copy.decrypt(key);
}
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("");
}
}
}
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());
}
}
}
}
public void storeWallet(Wallet wallet) throws IOException {
if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) {
storeWallet(encryptionPubKey, wallet);
return;
}
File parent = walletFile.getParentFile();
if(!parent.exists() && !parent.mkdirs()) { if(!parent.exists() && !parent.mkdirs()) {
throw new IOException("Could not create folder " + parent); throw new IOException("Could not create folder " + parent);
} }
if(!file.getName().endsWith(".json")) { if(!walletFile.getName().endsWith(".json")) {
File jsonFile = new File(parent, file.getName() + ".json"); File jsonFile = new File(parent, walletFile.getName() + ".json");
if(file.exists()) { if(walletFile.exists()) {
if(!file.renameTo(jsonFile)) { if(!walletFile.renameTo(jsonFile)) {
throw new IOException("Could not rename " + file.getName() + " to " + jsonFile.getName()); throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
} }
} }
file = jsonFile; walletFile = jsonFile;
} }
Writer writer = new FileWriter(file); Writer writer = new FileWriter(walletFile);
gson.toJson(wallet, writer); gson.toJson(wallet, writer);
writer.close(); writer.close();
} }
public void storeWallet(File file, ECKey encryptionKey, Wallet wallet) throws IOException { private void storeWallet(ECKey encryptionPubKey, Wallet wallet) throws IOException {
File parent = file.getParentFile(); File parent = walletFile.getParentFile();
if(!parent.exists() && !parent.mkdirs()) { if(!parent.exists() && !parent.mkdirs()) {
throw new IOException("Could not create folder " + parent); throw new IOException("Could not create folder " + parent);
} }
if(file.getName().endsWith(".json")) { if(walletFile.getName().endsWith(".json")) {
File noJsonFile = new File(parent, file.getName().substring(0, file.getName().lastIndexOf('.'))); File noJsonFile = new File(parent, walletFile.getName().substring(0, walletFile.getName().lastIndexOf('.')));
if(file.exists()) { if(walletFile.exists()) {
if(!file.renameTo(noJsonFile)) { if(!walletFile.renameTo(noJsonFile)) {
throw new IOException("Could not rename " + file.getName() + " to " + noJsonFile.getName()); throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
} }
} }
file = noJsonFile; walletFile = noJsonFile;
} }
OutputStreamWriter writer = new OutputStreamWriter(new DeflaterOutputStream(new ECIESOutputStream(new FileOutputStream(file), encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8); OutputStream outputStream = new FileOutputStream(walletFile);
writeBinaryHeader(outputStream);
OutputStreamWriter writer = new OutputStreamWriter(new DeflaterOutputStream(new ECIESOutputStream(outputStream, encryptionPubKey, getEncryptionMagic())), StandardCharsets.UTF_8);
gson.toJson(wallet, writer); gson.toJson(wallet, writer);
writer.close(); writer.close();
} }
private void writeBinaryHeader(OutputStream outputStream) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(21);
buf.put(HEADER_MAGIC_1.getBytes(StandardCharsets.UTF_8));
buf.put(keyDeriver.getSalt());
byte[] encoded = Base64.getEncoder().encode(buf.array());
if(encoded.length != BINARY_HEADER_LENGTH) {
throw new IllegalStateException("Header length not " + BINARY_HEADER_LENGTH + " bytes");
}
outputStream.write(encoded);
}
public ECKey getEncryptionPubKey() {
return encryptionPubKey;
}
public void setEncryptionPubKey(ECKey encryptionPubKey) {
this.encryptionPubKey = encryptionPubKey;
}
public ECKey getEncryptionKey(String password) throws IOException, StorageException {
return getEncryptionKey(password, null);
}
private ECKey getEncryptionKey(String password, InputStream inputStream) throws IOException, StorageException {
if(password.equals("")) {
return NO_PASSWORD_KEY;
}
return getKeyDeriver(inputStream).deriveECKey(password);
}
public AsymmetricKeyDeriver getKeyDeriver() {
return keyDeriver;
}
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
this.keyDeriver = keyDeriver;
}
private AsymmetricKeyDeriver getKeyDeriver(InputStream inputStream) throws IOException, StorageException {
if(keyDeriver == null) {
byte[] salt = new byte[SPRW1_PARAMETERS.saltLength];
if(inputStream != null) {
byte[] header = new byte[BINARY_HEADER_LENGTH];
int read = inputStream.read(header);
if(read != BINARY_HEADER_LENGTH) {
throw new StorageException("Not a Sparrow wallet - invalid header");
}
byte[] decodedHeader = Base64.getDecoder().decode(header);
byte[] magic = Arrays.copyOfRange(decodedHeader, 0, HEADER_MAGIC_1.length());
if(!HEADER_MAGIC_1.equals(new String(magic, StandardCharsets.UTF_8))) {
throw new StorageException("Not a Sparrow wallet - invalid magic");
}
salt = Arrays.copyOfRange(decodedHeader, HEADER_MAGIC_1.length(), decodedHeader.length);
} else {
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(salt);
}
keyDeriver = new Argon2KeyDeriver(salt);
}
return keyDeriver;
}
private static byte[] getEncryptionMagic() { private static byte[] getEncryptionMagic() {
return "BIE1".getBytes(StandardCharsets.UTF_8); return "BIE1".getBytes(StandardCharsets.UTF_8);
} }
public File getWalletFile(String walletName) { public static File getWalletFile(String walletName) {
//TODO: Check for existing file
return new File(getWalletsDir(), walletName); return new File(getWalletsDir(), walletName);
} }
public File getWalletsDir() { public static File getWalletsDir() {
File walletsDir = new File(getSparrowDir(), WALLETS_DIR); File walletsDir = new File(getSparrowDir(), WALLETS_DIR);
if(!walletsDir.exists()) { if(!walletsDir.exists()) {
walletsDir.mkdirs(); walletsDir.mkdirs();
@ -124,11 +263,11 @@ public class Storage {
return walletsDir; return walletsDir;
} }
private File getSparrowDir() { private static File getSparrowDir() {
return new File(getHomeDir(), SPARROW_DIR); return new File(getHomeDir(), SPARROW_DIR);
} }
private File getHomeDir() { private static File getHomeDir() {
return new File(System.getProperty("user.home")); return new File(System.getProperty("user.home"));
} }

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.io;
public class StorageException extends Exception {
public StorageException() {
super();
}
public StorageException(String message) {
super(message);
}
public StorageException(Throwable cause) {
super(cause);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -1,8 +1,7 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver;
import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
@ -16,7 +15,7 @@ import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog; import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent; import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletChangedEvent; import com.sparrowwallet.sparrow.event.WalletChangedEvent;
import javafx.application.Platform; import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -159,9 +158,9 @@ public class SettingsController extends WalletFormController implements Initiali
apply.setOnAction(event -> { apply.setOnAction(event -> {
try { try {
Optional<ECKey> optionalPubKey = requestEncryption(walletForm.getEncryptionPubKey()); Optional<ECKey> optionalPubKey = requestEncryption(walletForm.getStorage().getEncryptionPubKey());
if(optionalPubKey.isPresent()) { if(optionalPubKey.isPresent()) {
walletForm.setEncryptionPubKey(optionalPubKey.get()); walletForm.getStorage().setEncryptionPubKey(optionalPubKey.get());
walletForm.save(); walletForm.save();
revert.setDisable(true); revert.setDisable(true);
apply.setDisable(true); apply.setDisable(true);
@ -260,7 +259,7 @@ public class SettingsController extends WalletFormController implements Initiali
WalletPasswordDialog.PasswordRequirement requirement; WalletPasswordDialog.PasswordRequirement requirement;
if(existingPubKey == null) { if(existingPubKey == null) {
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_NEW; requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_NEW;
} else if(WalletForm.NO_PASSWORD_KEY.equals(existingPubKey)) { } else if(Storage.NO_PASSWORD_KEY.equals(existingPubKey)) {
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_EMPTY; requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_EMPTY;
} else { } else {
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_SET; requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_SET;
@ -270,19 +269,24 @@ public class SettingsController extends WalletFormController implements Initiali
Optional<String> password = dlg.showAndWait(); Optional<String> password = dlg.showAndWait();
if(password.isPresent()) { if(password.isPresent()) {
if(password.get().isEmpty()) { if(password.get().isEmpty()) {
return Optional.of(WalletForm.NO_PASSWORD_KEY); return Optional.of(Storage.NO_PASSWORD_KEY);
} }
ECKey encryptionFullKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey(password.get()); try {
ECKey encryptionFullKey = walletForm.getStorage().getEncryptionKey(password.get());
ECKey encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); ECKey encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey);
if(existingPubKey != null && !WalletForm.NO_PASSWORD_KEY.equals(existingPubKey) && !existingPubKey.equals(encryptionPubKey)) { if(existingPubKey != null && !Storage.NO_PASSWORD_KEY.equals(existingPubKey) && !existingPubKey.equals(encryptionPubKey)) {
AppController.showErrorDialog("Incorrect Password", "The password was incorrect."); AppController.showErrorDialog("Incorrect Password", "The password was incorrect.");
return Optional.empty(); return Optional.empty();
} }
walletForm.getWallet().encrypt(password.get()); Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
walletForm.getWallet().encrypt(key);
return Optional.of(encryptionPubKey); return Optional.of(encryptionPubKey);
} catch (Exception e) {
AppController.showErrorDialog("Wallet File Invalid", e.getMessage());
}
} }
return Optional.empty(); return Optional.empty();

View file

@ -9,16 +9,12 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
public class WalletForm { public class WalletForm {
public static final ECKey NO_PASSWORD_KEY = ECKey.fromPublicOnly(Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey("")); private final Storage storage;
private final File walletFile;
private ECKey encryptionPubKey;
private Wallet oldWallet; private Wallet oldWallet;
private Wallet wallet; private Wallet wallet;
public WalletForm(File walletFile, ECKey encryptionPubKey, Wallet currentWallet) { public WalletForm(Storage storage, Wallet currentWallet) {
this.walletFile = walletFile; this.storage = storage;
this.encryptionPubKey = encryptionPubKey;
this.oldWallet = currentWallet; this.oldWallet = currentWallet;
this.wallet = currentWallet.copy(); this.wallet = currentWallet.copy();
} }
@ -27,16 +23,12 @@ public class WalletForm {
return wallet; return wallet;
} }
public Storage getStorage() {
return storage;
}
public File getWalletFile() { public File getWalletFile() {
return walletFile; return storage.getWalletFile();
}
public ECKey getEncryptionPubKey() {
return encryptionPubKey;
}
public void setEncryptionPubKey(ECKey encryptionPubKey) {
this.encryptionPubKey = encryptionPubKey;
} }
public void revert() { public void revert() {
@ -44,12 +36,7 @@ public class WalletForm {
} }
public void save() throws IOException { public void save() throws IOException {
if(encryptionPubKey.equals(NO_PASSWORD_KEY)) { storage.storeWallet(wallet);
Storage.getStorage().storeWallet(walletFile, wallet);
} else {
Storage.getStorage().storeWallet(walletFile, encryptionPubKey, wallet);
}
oldWallet = wallet.copy(); oldWallet = wallet.copy();
} }
} }

View file

@ -15,45 +15,46 @@ import java.io.*;
public class StorageTest extends IoTest { public class StorageTest extends IoTest {
@Test @Test
public void loadWallet() throws IOException { public void loadWallet() throws IOException, MnemonicException, StorageException {
ECKey decryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey("pass"); Storage storage = new Storage(getFile("sparrow-single-wallet"));
Wallet wallet = Storage.getStorage().loadWallet(getFile("sparrow-single-wallet"), decryptionKey); Wallet wallet = storage.loadWallet("pass");
Assert.assertTrue(wallet.isValid()); Assert.assertTrue(wallet.isValid());
} }
@Test @Test
public void loadSeedWallet() throws IOException, MnemonicException { public void loadSeedWallet() throws IOException, MnemonicException, StorageException {
ECKey decryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey("pass"); Storage storage = new Storage(getFile("sparrow-single-seed-wallet"));
Wallet wallet = storage.loadWallet("pass");
Wallet wallet = Storage.getStorage().loadWallet(getFile("sparrow-single-seed-wallet"), decryptionKey);
Assert.assertTrue(wallet.isValid()); Assert.assertTrue(wallet.isValid());
Assert.assertEquals("testa1", wallet.getName()); Assert.assertEquals("testd2", wallet.getName());
Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType()); Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2WPKH, wallet.getScriptType()); Assert.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
Assert.assertEquals(1, wallet.getDefaultPolicy().getNumSignaturesRequired()); Assert.assertEquals(1, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("pkh(60bcd3a7)", wallet.getDefaultPolicy().getMiniscript().getScript()); Assert.assertEquals("pkh(60bcd3a7)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("60bcd3a7", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); Assert.assertEquals("60bcd3a7", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/84'/0'/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()); Assert.assertEquals("m/84'/0'/3'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub6BrhGFTWPd3DQaGP7p5zTQkE5nqVbaRs23HNae8jAoNJYS2NGa9Sgpeqv1dS5ygwD4sQfwqLCk5qXRK45FTgnqHRcrPnts3Qgh78BZrnoMn", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); Assert.assertEquals("xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
Assert.assertEquals("a48767d6b58732a0cad17ed93e23022ec603a177e75461f2aed994713fbbe532b61f6c0758a8aedcf9b2b8102c01c6f3e3e212ca06f13644d4ac8dad66556e164b7eaf79d0b42eadecee8b735e97fc0a", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getEncryptedData().getEncryptedBytes())); Assert.assertEquals("af6ebd81714c301c3a71fe11a7a9c99ccef4b33d4b36582220767bfa92768a2aa040f88b015b2465f8075a8b9dbf892a7d6e6c49932109f2cbc05ba0bd7f355fbcc34c237f71be5fb4dd7f8184e44cb0", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getEncryptedData().getEncryptedBytes()));
Assert.assertNull(wallet.getKeystores().get(0).getSeed().getSeedBytes()); Assert.assertNull(wallet.getKeystores().get(0).getSeed().getMnemonicCode());
} }
@Test @Test
public void saveWallet() throws IOException { public void saveWallet() throws IOException, MnemonicException, StorageException {
ECKey decryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey("pass"); Storage storage = new Storage(getFile("sparrow-single-wallet"));
Wallet wallet = Storage.getStorage().loadWallet(getFile("sparrow-single-wallet"), decryptionKey); Wallet wallet = storage.loadWallet("pass");
Assert.assertTrue(wallet.isValid()); Assert.assertTrue(wallet.isValid());
ECKey encyptionKey = ECKey.fromPublicOnly(decryptionKey);
File tempWallet = File.createTempFile("sparrow", "tmp"); File tempWallet = File.createTempFile("sparrow", "tmp");
tempWallet.deleteOnExit(); tempWallet.deleteOnExit();
ByteArrayOutputStream dummyFileOutputStream = new ByteArrayOutputStream(); Storage tempStorage = new Storage(tempWallet);
Storage.getStorage().storeWallet(tempWallet, encyptionKey, wallet); tempStorage.setKeyDeriver(storage.getKeyDeriver());
tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey());
tempStorage.storeWallet(wallet);
wallet = Storage.getStorage().loadWallet(tempWallet, decryptionKey); Storage temp2Storage = new Storage(tempWallet);
wallet = temp2Storage.loadWallet("pass");
Assert.assertTrue(wallet.isValid()); Assert.assertTrue(wallet.isValid());
} }
} }

View file

@ -1 +1 @@
QklFMQOXQWixo1F4kqJjn/7UeGDmbpNdMGPUP+vzRWdUE2vOcHfejUklKAzq1cTpK0mMEOOHCZ+yg0LxF29KMFQpoSpRTl6GRjnmpGIu3HPuu7bjbL6GvZQXFkvmK+9zsrllxZl6sYMjP4zoHJH6JgL4qGFpnM9n/mtOGYAuOw7zj4fm10teHLLgJLpkZ4ze0n/t8QhbyPPkeEoNmw/gt1PrwNDneKtALCNNdGopEq0QWD15OCEY7fSIfu0K1VO9oMZpOHs78p335Ka7bRRjuq+RWvZLz/X5hb9zVlFIu+KLW/preykMbfg4UiPcVHfc9wSLsmqZe9btYa3yxsem9xRW7J4gXMr7uqMTw/dlrK6XNg0wBXH6cuBed3M6Nu72Hz0OU+M64HEUnsGRYLgz3XcsZAU7+jeUQp6D8sVjH7GsPFIdZl0wzhWNHsgQGLoGXnWyvatsPsklW9BQ5U1d0PxeLxWppwj42Y7YA0+O3BrN7lUmD6xATNt9/xwVotPIllXT84r/OpzFbULzSBZ0uwwV+4tdCOa7FHPUed5gXZCPk6lhPHkz2N6Ehm3WGOQBQsoTfObefYUjHhB+a6Rpggm7QgkLhBEtgy7sPCV39fnRYM52uOwDUhNB8K/h/RadkMX51eXtbx23+LU+jM8zpi4T6yLM1Rq8H9YZ+rJXUIZPOHwX2ytnXkwHcYanThhxvQa55v3vbC5X/JFVSA+yik1CejXctjv9I++5mCCJtGpk/USAWzIb3nhKURPi/a3sg7y/n47s79AxvzvLndlY96UIaI1QVaNoaoJtt1+cBLzrXGlc4hChIqloZN4GJ4F1KabDuyXwagGvsd14zTH6ELAprmJkrd0l9JNTEBrJJbRqFEZj7CwrREyUkxGaiskca+ZyHm4LuwKo/6m5dKgiqyeMUB6WdZFXDERSp9ldf8n+o6OSxolEq4wWxM2uGdbHZTBsORq0JyIS4CQcfiC4UQoXJnQpGAfKWNsc1jS/x0BlV/9n2rhGxC0LRQZ2YbtLwfwd138= U1BSVzED7oX1HJm5jfGejCdFrag8QklFMQJ5dd4sdqdFohYxy0Vnk3jdhUqPPRf6iAC2PPNDo67JfH6VpVDKR8PC1kcKsIYnKE05gA1e+EfgPB/yd1z3xCu8UcwRqFdFimPJQaa28YOFKFeHdwlul+xI7U7wNKB2QZYGe2HZ/SaFO1ccMwHrZ85AxHhCI/6Qfk52heYE0qv3dbechg84J73qmx95Rl1FiMt4THZp8fKN827QyMz4ReC/uixhXerDQm6nKDb5KaNh5/aZSY8Qw8jIIqtveDkwZl1F7tA3H54Yxa4Ugz/Mcr4A3/rEaCrtMGS89XUDhaQU4GznKyK4DM4A/jw7CWnPHreSHZ79FL1/xfHDnNPAKpKDOfNqoGIL/QzgegwZLFQTQj5z81fFw370pAgh17h0diaJ+Z3TQR++/olPdNZYMU0Rl+xCjwABXUPB2AcdiIcUniGWKgXiUTW3B+nttb3m1WV/HCm/knQd42sGsFf5ZbjweqRzObvfMSkGaTKkB6ickw2VIxJSjiYTNWF3MVunUbFaqrk94OGnR10leg58iXzDvoX0zhkVdFZOxX4VjCKYCQQNng1pUj9MGpov1q7BXAiS1/ABkgvsD3tBZllV2Eg+P78UeuYpAjBNZnEjvydZOP8b86Yey12SnsUZCmnIeeOvDnqi603XdjNojQVltncIgAxlp3Bmj3tRSxUM49akj0hBsZASW3V1JC/P6cScpgv7e5/DyXIAFz7+yGzJ1cYMwUtVMOQMMJSc9y2nSMrEqFNJR+U8d9kfp8KYwst6GsJLUKBdDQMI/20O0PZSM0pWMJFE9iPK/YNDmjC7F0p8pw==

View file

@ -1 +1 @@
QklFMQNI/quQo9N7RtbygK+yhlrMNxkSnXtlaC9Ia5trm7AufOtbKhGqrtv5bQ/YcRVVaj/eKhO7LWTGbC6EWFYbIle/tpTyQB5XdceCCWmbUDwyob+thVpMLLrVe9PQD+EH6GM2cWGFUZNMHdYM2N/EaLU4Z2nnDz9pLzg1jpOtU9n3D1IeivULxfkupsd0AqxkpkXJlc0y7udh2qzXk/BPffYkEN0NexspO2+I1+o81g1IcVRXNV7LR8o/woKRM4MPBhUNVOy2F5JyvKnsteBKpEpKa4AyHmhGRtIdyKIZK4+osIU9Ig+b/AItDj9OG354gpL7oiU65s7rF8UsJpDLtxIyONUL6becqsNNem0rTbHQ0PI1uoWHmQj8dUl8sqhdIwC13Hhnx0+M5ICrqs3gk5tkUyiCDA7684jrWLGRjUzUXRPmNJsWPqlnCD2+MY93dduMwbJqV1USrOZDXsMd9LuGAV+UqEDMuBRjwXDxXQldrIBp9QKYac1mKFvj9UOJr062T2gwGsSyKY2R6oCiGJPkOZjRoQ0HHwJukFYJgoRRI34Hnh49LUfJybv+VEfqz9VJZhWnDhCgcFZ9r1BwY4CZ U1BSVzEeO0vCCaJ0C+yv91xHOivxQklFMQM8Z+yJUyrrWWhzlBrAe2gLlAA7z7KlPMo7DYp5IRas2U0n7w9P2qz13wqmENZ2ilPmLwGrNpcFD8/re34I3oSudN5mjs+Z9xqx7xAHRrtXIiiLoKZS9p+v44AKQxBBrNgvj6seH2n2mJ+TUYVhp0s5IKzeFAPewfMzVKnmt7VWUzzug+uPUoyS44RapDqSM1u1GtkA4+fSGTdE8GF0mG4Y0KvKi5ZtLgaFxe/c/7bX4XlQdbnzzYpFxklUJQYkeWn9N3OmC8kJo97IGGGCUOd6bIwZtTj9KGG/whrgcID12yB5L7uvVMSpFLv6Qs27+La/L8UuPSpnO+gr5r4K1OtG9kxQhksp+8Ap0yzPOJzOMGLYXv7pR+edO5Q5PTQ4K6dN6zdYgsTY6HA36N6qgNQLM5RwzXh1WINp3pI/FnEKeffGIrDPAHueKDHUd/Zw6rhXDbDn28oK+zf8lR57Rx78JFF7JczB0jYJreIE0mPwN2fn0iB7JQwUGt6XM5hsvHK1n/JxO9OELXYyMaB/t4ihEdiw6HosncdhTwkKLDXb11wwj845vjN4Q8pGq/1YOuuEYRe0eiaGQFXTaw9QYWPO