mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
refactor storage to handle different persistence strategies
This commit is contained in:
parent
8bc8bdb2f2
commit
fb72010bdf
11 changed files with 611 additions and 400 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit cc32285d58fcd1120b27797ff1d882ae4ae8a1ed
|
Subproject commit 7dca0d0c39404bd89a5a4589a79ffe0f688e8181
|
|
@ -740,13 +740,8 @@ public class AppController implements Initializable {
|
||||||
Storage storage = new Storage(file);
|
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)) {
|
||||||
Storage.WalletBackupAndKey walletBackupAndKey = storage.loadWallet();
|
WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet();
|
||||||
checkWalletNetwork(walletBackupAndKey.wallet);
|
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||||
restorePublicKeysFromSeed(walletBackupAndKey.wallet, null);
|
|
||||||
if(!walletBackupAndKey.wallet.isValid()) {
|
|
||||||
throw new IllegalStateException("Wallet file is not valid.");
|
|
||||||
}
|
|
||||||
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
|
|
||||||
} else if(FileType.BINARY.equals(fileType)) {
|
} else if(FileType.BINARY.equals(fileType)) {
|
||||||
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
||||||
|
@ -758,16 +753,8 @@ public class AppController implements Initializable {
|
||||||
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
|
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
|
||||||
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
||||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||||
Storage.WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
||||||
try {
|
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||||
checkWalletNetwork(walletBackupAndKey.wallet);
|
|
||||||
restorePublicKeysFromSeed(walletBackupAndKey.wallet, walletBackupAndKey.key);
|
|
||||||
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
|
|
||||||
} catch(Exception e) {
|
|
||||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
|
||||||
} finally {
|
|
||||||
walletBackupAndKey.key.clear();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
loadWalletService.setOnFailed(workerStateEvent -> {
|
loadWalletService.setOnFailed(workerStateEvent -> {
|
||||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||||
|
@ -798,6 +785,25 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openWallet(Storage storage, WalletBackupAndKey walletBackupAndKey, AppController appController, boolean forceSameWindow) {
|
||||||
|
try {
|
||||||
|
checkWalletNetwork(walletBackupAndKey.getWallet());
|
||||||
|
restorePublicKeysFromSeed(walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
|
||||||
|
if(!walletBackupAndKey.getWallet().isValid()) {
|
||||||
|
throw new IllegalStateException("Wallet file is not valid.");
|
||||||
|
}
|
||||||
|
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getBackupWallet(), forceSameWindow);
|
||||||
|
for(Map.Entry<Storage, WalletBackupAndKey> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||||
|
openWallet(entry.getKey(), entry.getValue(), walletAppController, true);
|
||||||
|
}
|
||||||
|
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
|
||||||
|
} catch(Exception e) {
|
||||||
|
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||||
|
} finally {
|
||||||
|
walletBackupAndKey.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void checkWalletNetwork(Wallet wallet) {
|
private void checkWalletNetwork(Wallet wallet) {
|
||||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||||
|
@ -937,7 +943,7 @@ public class AppController implements Initializable {
|
||||||
if(password.get().length() == 0) {
|
if(password.get().length() == 0) {
|
||||||
try {
|
try {
|
||||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||||
storage.storeWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
addWalletTabOrWindow(storage, wallet, null, false);
|
addWalletTabOrWindow(storage, wallet, null, false);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
log.error("Error saving imported wallet", e);
|
log.error("Error saving imported wallet", e);
|
||||||
|
@ -954,7 +960,7 @@ public class AppController implements Initializable {
|
||||||
key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||||
wallet.encrypt(key);
|
wallet.encrypt(key);
|
||||||
storage.setEncryptionPubKey(encryptionPubKey);
|
storage.setEncryptionPubKey(encryptionPubKey);
|
||||||
storage.storeWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
addWalletTabOrWindow(storage, wallet, null, false);
|
addWalletTabOrWindow(storage, wallet, null, false);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
log.error("Error saving imported wallet", e);
|
log.error("Error saving imported wallet", e);
|
||||||
|
@ -1032,14 +1038,14 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
|
public AppController addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
|
||||||
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage);
|
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage);
|
||||||
if(existingWalletWindow instanceof Stage) {
|
if(existingWalletWindow instanceof Stage) {
|
||||||
Stage existingWalletStage = (Stage)existingWalletWindow;
|
Stage existingWalletStage = (Stage)existingWalletWindow;
|
||||||
existingWalletStage.toFront();
|
existingWalletStage.toFront();
|
||||||
|
|
||||||
EventManager.get().post(new ViewWalletEvent(existingWalletWindow, wallet, storage));
|
EventManager.get().post(new ViewWalletEvent(existingWalletWindow, wallet, storage));
|
||||||
return;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!forceSameWindow && Config.get().isOpenWalletsInNewWindows() && !getOpenWallets().isEmpty()) {
|
if(!forceSameWindow && Config.get().isOpenWalletsInNewWindows() && !getOpenWallets().isEmpty()) {
|
||||||
|
@ -1048,8 +1054,10 @@ public class AppController implements Initializable {
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
stage.setX(AppServices.get().getWalletWindowMaxX() + 30);
|
stage.setX(AppServices.get().getWalletWindowMaxX() + 30);
|
||||||
appController.addWalletTab(storage, wallet, backupWallet);
|
appController.addWalletTab(storage, wallet, backupWallet);
|
||||||
|
return appController;
|
||||||
} else {
|
} else {
|
||||||
addWalletTab(storage, wallet, backupWallet);
|
addWalletTab(storage, wallet, backupWallet);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -825,7 +825,7 @@ public class AppServices {
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if(!Window.getWindows().isEmpty()) {
|
if(!Window.getWindows().isEmpty()) {
|
||||||
List<File> walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
|
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
|
||||||
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
|
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,7 +36,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||||
@Override
|
@Override
|
||||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||||
ColdcardKeystore cck = Storage.getGson().fromJson(reader, ColdcardKeystore.class);
|
ColdcardKeystore cck = JsonPersistence.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||||
|
|
||||||
Keystore keystore = new Keystore("Coldcard");
|
Keystore keystore = new Keystore("Coldcard");
|
||||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||||
|
|
412
src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java
Normal file
412
src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
|
||||||
|
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.zip.DeflaterOutputStream;
|
||||||
|
import java.util.zip.InflaterInputStream;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS;
|
||||||
|
|
||||||
|
public class JsonPersistence implements Persistence {
|
||||||
|
public static final String HEADER_MAGIC_1 = "SPRW1";
|
||||||
|
public static final int BINARY_HEADER_LENGTH = 28;
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
private AsymmetricKeyDeriver keyDeriver;
|
||||||
|
|
||||||
|
public JsonPersistence() {
|
||||||
|
this.gson = getGson();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet loadWallet(File jsonFile) throws IOException {
|
||||||
|
try(Reader reader = new FileReader(jsonFile)) {
|
||||||
|
return gson.fromJson(reader, Wallet.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletBackupAndKey loadWallet(File walletFile, CharSequence password) throws IOException, StorageException {
|
||||||
|
Wallet wallet;
|
||||||
|
ECKey encryptionKey;
|
||||||
|
|
||||||
|
try(InputStream fileStream = new FileInputStream(walletFile)) {
|
||||||
|
encryptionKey = getEncryptionKey(password, fileStream, null);
|
||||||
|
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||||
|
wallet = gson.fromJson(reader, Wallet.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WalletBackupAndKey(wallet, null, encryptionKey, keyDeriver, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<File, Wallet> loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException {
|
||||||
|
Map<File, Wallet> walletsMap = new LinkedHashMap<>();
|
||||||
|
for(File file : walletFiles) {
|
||||||
|
if(encryptionKey != null) {
|
||||||
|
try(InputStream fileStream = new FileInputStream(file)) {
|
||||||
|
encryptionKey = getEncryptionKey(null, fileStream, encryptionKey);
|
||||||
|
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||||
|
walletsMap.put(file, gson.fromJson(reader, Wallet.class));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
walletsMap.put(file, loadWallet(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return walletsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Storage, WalletBackupAndKey> loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||||
|
File[] walletFiles = getChildWalletFiles(walletFile, masterWallet);
|
||||||
|
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||||
|
Map<File, Wallet> loadedWallets = loadWallets(walletFiles, encryptionKey);
|
||||||
|
for(Map.Entry<File, Wallet> entry : loadedWallets.entrySet()) {
|
||||||
|
Storage storage = new Storage(entry.getKey());
|
||||||
|
storage.setEncryptionPubKey(ECKey.fromPublicOnly(encryptionKey));
|
||||||
|
storage.setKeyDeriver(getKeyDeriver());
|
||||||
|
Wallet childWallet = entry.getValue();
|
||||||
|
childWallet.setMasterWallet(masterWallet);
|
||||||
|
childWallets.put(storage, new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return childWallets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File[] getChildWalletFiles(File walletFile, Wallet masterWallet) {
|
||||||
|
File childDir = new File(walletFile.getParentFile(), masterWallet.getName() + "-child");
|
||||||
|
if(!childDir.exists()) {
|
||||||
|
return new File[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
File[] childFiles = childDir.listFiles(pathname -> {
|
||||||
|
FileType fileType = IOUtils.getFileType(pathname);
|
||||||
|
return pathname.getName().startsWith(masterWallet.getName()) && (fileType == FileType.BINARY || fileType == FileType.JSON);
|
||||||
|
});
|
||||||
|
|
||||||
|
return childFiles == null ? new File[0] : childFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File storeWallet(File walletFile, Wallet wallet) throws IOException {
|
||||||
|
File parent = walletFile.getParentFile();
|
||||||
|
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||||
|
throw new IOException("Could not create folder " + parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!walletFile.getName().endsWith(".json")) {
|
||||||
|
File jsonFile = new File(parent, walletFile.getName() + ".json");
|
||||||
|
if(walletFile.exists()) {
|
||||||
|
if(!walletFile.renameTo(jsonFile)) {
|
||||||
|
throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walletFile = jsonFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!walletFile.exists()) {
|
||||||
|
Storage.createOwnerOnlyFile(walletFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
try(Writer writer = new FileWriter(walletFile)) {
|
||||||
|
gson.toJson(wallet, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return walletFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||||
|
File parent = walletFile.getParentFile();
|
||||||
|
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||||
|
throw new IOException("Could not create folder " + parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(walletFile.getName().endsWith(".json")) {
|
||||||
|
File noJsonFile = new File(parent, walletFile.getName().substring(0, walletFile.getName().lastIndexOf('.')));
|
||||||
|
if(walletFile.exists()) {
|
||||||
|
if(!walletFile.renameTo(noJsonFile)) {
|
||||||
|
throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walletFile = noJsonFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!walletFile.exists()) {
|
||||||
|
Storage.createOwnerOnlyFile(walletFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
try(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);
|
||||||
|
//Close the writer explicitly as the try-resources block will not do so
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return walletFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getEncryptionMagic() {
|
||||||
|
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||||
|
return getEncryptionKey(password, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ECKey getEncryptionKey(CharSequence password, InputStream inputStream, ECKey alreadyDerivedKey) throws IOException, StorageException {
|
||||||
|
if(password != null && password.equals("")) {
|
||||||
|
return Storage.NO_PASSWORD_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsymmetricKeyDeriver keyDeriver = getKeyDeriver(inputStream);
|
||||||
|
return alreadyDerivedKey == null ? keyDeriver.deriveECKey(password) : alreadyDerivedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsymmetricKeyDeriver getKeyDeriver() {
|
||||||
|
return keyDeriver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
||||||
|
this.keyDeriver = keyDeriver;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsymmetricKeyDeriver getKeyDeriver(InputStream inputStream) throws IOException, StorageException {
|
||||||
|
if(keyDeriver == null) {
|
||||||
|
keyDeriver = getWalletKeyDeriver(inputStream);
|
||||||
|
} else if(inputStream != null) {
|
||||||
|
inputStream.skip(BINARY_HEADER_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyDeriver;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsymmetricKeyDeriver getWalletKeyDeriver(InputStream inputStream) throws IOException, StorageException {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
} catch(IllegalArgumentException e) {
|
||||||
|
throw new StorageException("Not a Sparrow wallet - invalid header");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SecureRandom secureRandom = new SecureRandom();
|
||||||
|
secureRandom.nextBytes(salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Argon2KeyDeriver(salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersistenceType getType() {
|
||||||
|
return PersistenceType.JSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Gson getGson() {
|
||||||
|
return getGson(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Gson getGson(boolean includeWalletSerializers) {
|
||||||
|
GsonBuilder gsonBuilder = new GsonBuilder();
|
||||||
|
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeySerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeyDeserializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArraySerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArrayDeserializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashSerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashDeserializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(Date.class, new DateSerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionSerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionDeserializer());
|
||||||
|
if(includeWalletSerializers) {
|
||||||
|
gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer());
|
||||||
|
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeDeserializer());
|
||||||
|
}
|
||||||
|
|
||||||
|
gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||||
|
@Override
|
||||||
|
public boolean shouldSkipField(FieldAttributes field) {
|
||||||
|
return field.getDeclaringClass() == Wallet.class && field.getName().equals("masterWallet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldSkipClass(Class<?> clazz) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ExtendedPublicKeySerializer implements JsonSerializer<ExtendedKey> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(ExtendedKey src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(src.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ExtendedPublicKeyDeserializer implements JsonDeserializer<ExtendedKey> {
|
||||||
|
@Override
|
||||||
|
public ExtendedKey deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return ExtendedKey.fromDescriptor(json.getAsJsonPrimitive().getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ByteArraySerializer implements JsonSerializer<byte[]> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(Utils.bytesToHex(src));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ByteArrayDeserializer implements JsonDeserializer<byte[]> {
|
||||||
|
@Override
|
||||||
|
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha256HashSerializer implements JsonSerializer<Sha256Hash> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Sha256Hash src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(src.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha256HashDeserializer implements JsonDeserializer<Sha256Hash> {
|
||||||
|
@Override
|
||||||
|
public Sha256Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return Sha256Hash.wrap(json.getAsJsonPrimitive().getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DateSerializer implements JsonSerializer<Date> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(src.getTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DateDeserializer implements JsonDeserializer<Date> {
|
||||||
|
@Override
|
||||||
|
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return new Date(json.getAsJsonPrimitive().getAsLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TransactionSerializer implements JsonSerializer<Transaction> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Transaction src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
src.bitcoinSerializeToStream(baos);
|
||||||
|
return new JsonPrimitive(Utils.bytesToHex(baos.toByteArray()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TransactionDeserializer implements JsonDeserializer<Transaction> {
|
||||||
|
@Override
|
||||||
|
public Transaction deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
byte[] rawTx = Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
||||||
|
return new Transaction(rawTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class KeystoreSerializer implements JsonSerializer<Keystore> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore);
|
||||||
|
if(keystore.hasPrivateKey()) {
|
||||||
|
jsonObject.remove("extendedPublicKey");
|
||||||
|
jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint");
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NodeSerializer implements JsonSerializer<WalletNode> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(WalletNode node, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(node);
|
||||||
|
|
||||||
|
JsonArray children = jsonObject.getAsJsonArray("children");
|
||||||
|
Iterator<JsonElement> iter = children.iterator();
|
||||||
|
while(iter.hasNext()) {
|
||||||
|
JsonObject childObject = (JsonObject)iter.next();
|
||||||
|
removeEmptyCollection(childObject, "children");
|
||||||
|
removeEmptyCollection(childObject, "transactionOutputs");
|
||||||
|
|
||||||
|
if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("transactionOutputs") == null) {
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeEmptyCollection(JsonObject jsonObject, String memberName) {
|
||||||
|
if(jsonObject.get(memberName) != null && jsonObject.getAsJsonArray(memberName).size() == 0) {
|
||||||
|
jsonObject.remove(memberName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NodeDeserializer implements JsonDeserializer<WalletNode> {
|
||||||
|
@Override
|
||||||
|
public WalletNode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
WalletNode node = getGson(false).fromJson(json, typeOfT);
|
||||||
|
node.parseDerivation();
|
||||||
|
|
||||||
|
for(WalletNode childNode : node.getChildren()) {
|
||||||
|
childNode.parseDerivation();
|
||||||
|
if(childNode.getChildren() == null) {
|
||||||
|
childNode.setChildren(new TreeSet<>());
|
||||||
|
}
|
||||||
|
if(childNode.getTransactionOutputs() == null) {
|
||||||
|
childNode.setTransactionOutputs(new TreeSet<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
src/main/java/com/sparrowwallet/sparrow/io/Persistence.java
Normal file
22
src/main/java/com/sparrowwallet/sparrow/io/Persistence.java
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface Persistence {
|
||||||
|
Wallet loadWallet(File walletFile) throws IOException;
|
||||||
|
WalletBackupAndKey loadWallet(File walletFile, CharSequence password) throws IOException, StorageException;
|
||||||
|
Map<File, Wallet> loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException;
|
||||||
|
Map<Storage, WalletBackupAndKey> loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException;
|
||||||
|
File storeWallet(File walletFile, Wallet wallet) throws IOException;
|
||||||
|
File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException;
|
||||||
|
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
|
||||||
|
AsymmetricKeyDeriver getKeyDeriver();
|
||||||
|
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
||||||
|
PersistenceType getType();
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
public enum PersistenceType {
|
||||||
|
JSON("json");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private PersistenceType(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExtension() {
|
||||||
|
return getName();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,10 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.google.gson.*;
|
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.crypto.*;
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
|
||||||
import com.sparrowwallet.sparrow.MainApp;
|
import com.sparrowwallet.sparrow.MainApp;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
|
@ -19,13 +13,9 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.attribute.PosixFilePermission;
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.nio.file.attribute.PosixFilePermissions;
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
@ -33,9 +23,6 @@ import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.*;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS;
|
|
||||||
|
|
||||||
public class Storage {
|
public class Storage {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Storage.class);
|
private static final Logger log = LoggerFactory.getLogger(Storage.class);
|
||||||
|
@ -49,173 +36,60 @@ public class Storage {
|
||||||
public static final String WALLETS_DIR = "wallets";
|
public static final String WALLETS_DIR = "wallets";
|
||||||
public static final String WALLETS_BACKUP_DIR = "backup";
|
public static final String WALLETS_BACKUP_DIR = "backup";
|
||||||
public static final String CERTS_DIR = "certs";
|
public static final String CERTS_DIR = "certs";
|
||||||
public static final String HEADER_MAGIC_1 = "SPRW1";
|
|
||||||
private static final int BINARY_HEADER_LENGTH = 28;
|
|
||||||
public static final String TEMP_BACKUP_EXTENSION = "tmp";
|
public static final String TEMP_BACKUP_EXTENSION = "tmp";
|
||||||
|
|
||||||
|
private final Persistence persistence;
|
||||||
private File walletFile;
|
private File walletFile;
|
||||||
private final Gson gson;
|
|
||||||
private AsymmetricKeyDeriver keyDeriver;
|
|
||||||
private ECKey encryptionPubKey;
|
private ECKey encryptionPubKey;
|
||||||
|
|
||||||
public Storage(File walletFile) {
|
public Storage(File walletFile) {
|
||||||
|
this.persistence = new JsonPersistence();
|
||||||
this.walletFile = walletFile;
|
this.walletFile = walletFile;
|
||||||
this.gson = getGson();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getWalletFile() {
|
public File getWalletFile() {
|
||||||
return walletFile;
|
return walletFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Gson getGson() {
|
public WalletBackupAndKey loadUnencryptedWallet() throws IOException, StorageException {
|
||||||
return getGson(true);
|
Wallet wallet = persistence.loadWallet(walletFile);
|
||||||
}
|
Wallet backupWallet = loadBackupWallet(null);
|
||||||
|
Map<Storage, WalletBackupAndKey> childWallets = persistence.loadChildWallets(walletFile, wallet, null);
|
||||||
private static Gson getGson(boolean includeWalletSerializers) {
|
|
||||||
GsonBuilder gsonBuilder = new GsonBuilder();
|
|
||||||
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeySerializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeyDeserializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArraySerializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArrayDeserializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashSerializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashDeserializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(Date.class, new DateSerializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionSerializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionDeserializer());
|
|
||||||
if(includeWalletSerializers) {
|
|
||||||
gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer());
|
|
||||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeDeserializer());
|
|
||||||
}
|
|
||||||
|
|
||||||
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public WalletBackupAndKey loadWallet() throws IOException {
|
|
||||||
Wallet wallet = loadWallet(walletFile);
|
|
||||||
|
|
||||||
Wallet backupWallet = null;
|
|
||||||
File[] backups = getBackups("json." + TEMP_BACKUP_EXTENSION);
|
|
||||||
if(backups.length > 0) {
|
|
||||||
try {
|
|
||||||
backupWallet = loadWallet(backups[0]);
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptionPubKey = NO_PASSWORD_KEY;
|
encryptionPubKey = NO_PASSWORD_KEY;
|
||||||
return new WalletBackupAndKey(wallet, backupWallet, null);
|
return new WalletBackupAndKey(wallet, backupWallet, null, null, childWallets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Wallet loadWallet(File jsonFile) throws IOException {
|
public WalletBackupAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
|
||||||
Reader reader = new FileReader(jsonFile);
|
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(walletFile, password);
|
||||||
Wallet wallet = gson.fromJson(reader, Wallet.class);
|
Wallet backupWallet = loadBackupWallet(masterWalletAndKey.getEncryptionKey());
|
||||||
reader.close();
|
Map<Storage, WalletBackupAndKey> childWallets = persistence.loadChildWallets(walletFile, masterWalletAndKey.getWallet(), masterWalletAndKey.getEncryptionKey());
|
||||||
|
|
||||||
return wallet;
|
encryptionPubKey = ECKey.fromPublicOnly(masterWalletAndKey.getEncryptionKey());
|
||||||
|
return new WalletBackupAndKey(masterWalletAndKey.getWallet(), backupWallet, masterWalletAndKey.getEncryptionKey(), persistence.getKeyDeriver(), childWallets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletBackupAndKey loadWallet(CharSequence password) throws IOException, StorageException {
|
protected Wallet loadBackupWallet(ECKey encryptionKey) throws IOException, StorageException {
|
||||||
WalletAndKey walletAndKey = loadWallet(walletFile, password);
|
Map<File, Wallet> backupWallets;
|
||||||
|
if(encryptionKey != null) {
|
||||||
WalletAndKey backupAndKey = new WalletAndKey(null, null);
|
File[] backups = getBackups(TEMP_BACKUP_EXTENSION, persistence.getType().getExtension() + "." + TEMP_BACKUP_EXTENSION);
|
||||||
File[] backups = getBackups(TEMP_BACKUP_EXTENSION, "json." + TEMP_BACKUP_EXTENSION);
|
backupWallets = persistence.loadWallets(backups, encryptionKey);
|
||||||
if(backups.length > 0) {
|
return backupWallets.isEmpty() ? null : backupWallets.values().iterator().next();
|
||||||
try {
|
} else {
|
||||||
backupAndKey = loadWallet(backups[0], password);
|
File[] backups = getBackups(persistence.getType().getExtension() + "." + TEMP_BACKUP_EXTENSION);
|
||||||
} catch(Exception e) {
|
backupWallets = persistence.loadWallets(backups, null);
|
||||||
log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WalletBackupAndKey(walletAndKey.wallet, backupAndKey.wallet, walletAndKey.key);
|
return backupWallets.isEmpty() ? null : backupWallets.values().iterator().next();
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletAndKey loadWallet(File encryptedFile, CharSequence password) throws IOException, StorageException {
|
public void saveWallet(Wallet wallet) throws IOException {
|
||||||
InputStream fileStream = new FileInputStream(encryptedFile);
|
|
||||||
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);
|
|
||||||
reader.close();
|
|
||||||
|
|
||||||
Key key = new Key(encryptionKey.getPrivKeyBytes(), keyDeriver.getSalt(), EncryptionType.Deriver.ARGON2);
|
|
||||||
|
|
||||||
encryptionPubKey = ECKey.fromPublicOnly(encryptionKey);
|
|
||||||
return new WalletAndKey(wallet, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void storeWallet(Wallet wallet) throws IOException {
|
|
||||||
if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) {
|
if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) {
|
||||||
storeWallet(encryptionPubKey, wallet);
|
walletFile = persistence.storeWallet(walletFile, wallet, encryptionPubKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File parent = walletFile.getParentFile();
|
walletFile = persistence.storeWallet(walletFile, wallet);
|
||||||
if(!parent.exists() && !createOwnerOnlyDirectory(parent)) {
|
|
||||||
throw new IOException("Could not create folder " + parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!walletFile.getName().endsWith(".json")) {
|
|
||||||
File jsonFile = new File(parent, walletFile.getName() + ".json");
|
|
||||||
if(walletFile.exists()) {
|
|
||||||
if(!walletFile.renameTo(jsonFile)) {
|
|
||||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walletFile = jsonFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!walletFile.exists()) {
|
|
||||||
createOwnerOnlyFile(walletFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
Writer writer = new FileWriter(walletFile);
|
|
||||||
gson.toJson(wallet, writer);
|
|
||||||
writer.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void storeWallet(ECKey encryptionPubKey, Wallet wallet) throws IOException {
|
|
||||||
File parent = walletFile.getParentFile();
|
|
||||||
if(!parent.exists() && !createOwnerOnlyDirectory(parent)) {
|
|
||||||
throw new IOException("Could not create folder " + parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(walletFile.getName().endsWith(".json")) {
|
|
||||||
File noJsonFile = new File(parent, walletFile.getName().substring(0, walletFile.getName().lastIndexOf('.')));
|
|
||||||
if(walletFile.exists()) {
|
|
||||||
if(!walletFile.renameTo(noJsonFile)) {
|
|
||||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walletFile = noJsonFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!walletFile.exists()) {
|
|
||||||
createOwnerOnlyFile(walletFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
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 void backupWallet() throws IOException {
|
public void backupWallet() throws IOException {
|
||||||
|
@ -232,7 +106,7 @@ public class Storage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backupWallet(String extension) throws IOException {
|
private void backupWallet(String extension) throws IOException {
|
||||||
File backupDir = getWalletsBackupDir();
|
File backupDir = getWalletsBackupDir();
|
||||||
|
|
||||||
Date backupDate = new Date();
|
Date backupDate = new Date();
|
||||||
|
@ -260,7 +134,11 @@ public class Storage {
|
||||||
deleteBackups(null);
|
deleteBackups(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteBackups(String extension) {
|
public void deleteTempBackups() {
|
||||||
|
deleteBackups(Storage.TEMP_BACKUP_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteBackups(String extension) {
|
||||||
File[] backups = getBackups(extension);
|
File[] backups = getBackups(extension);
|
||||||
for(File backup : backups) {
|
for(File backup : backups) {
|
||||||
backup.delete();
|
backup.delete();
|
||||||
|
@ -280,6 +158,7 @@ public class Storage {
|
||||||
(notExtension == null || !name.endsWith("." + notExtension));
|
(notExtension == null || !name.endsWith("." + notExtension));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
backups = backups == null ? new File[0] : backups;
|
||||||
Arrays.sort(backups, Comparator.comparing(o -> getBackupDate(((File)o).getName())).reversed());
|
Arrays.sort(backups, Comparator.comparing(o -> getBackupDate(((File)o).getName())).reversed());
|
||||||
|
|
||||||
return backups;
|
return backups;
|
||||||
|
@ -303,78 +182,45 @@ public class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||||
return getEncryptionKey(password, null);
|
return persistence.getEncryptionKey(password);
|
||||||
}
|
|
||||||
|
|
||||||
private ECKey getEncryptionKey(CharSequence password, InputStream inputStream) throws IOException, StorageException {
|
|
||||||
if(password.equals("")) {
|
|
||||||
return NO_PASSWORD_KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getKeyDeriver(inputStream).deriveECKey(password);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AsymmetricKeyDeriver getKeyDeriver() {
|
public AsymmetricKeyDeriver getKeyDeriver() {
|
||||||
return keyDeriver;
|
return persistence.getKeyDeriver();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
||||||
this.keyDeriver = keyDeriver;
|
persistence.setKeyDeriver(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");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
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);
|
|
||||||
} catch(IllegalArgumentException e) {
|
|
||||||
throw new StorageException("Not a Sparrow wallet - invalid header");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SecureRandom secureRandom = new SecureRandom();
|
|
||||||
secureRandom.nextBytes(salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
keyDeriver = new Argon2KeyDeriver(salt);
|
|
||||||
} else if(inputStream != null) {
|
|
||||||
inputStream.skip(BINARY_HEADER_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyDeriver;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] getEncryptionMagic() {
|
|
||||||
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean walletExists(String walletName) {
|
public static boolean walletExists(String walletName) {
|
||||||
File encrypted = new File(getWalletsDir(), walletName.trim());
|
File encrypted = new File(getWalletsDir(), walletName.trim());
|
||||||
File unencrypted = new File(getWalletsDir(), walletName.trim() + ".json");
|
if(encrypted.exists()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return (encrypted.exists() || unencrypted.exists());
|
for(PersistenceType persistenceType : PersistenceType.values()) {
|
||||||
|
File unencrypted = new File(getWalletsDir(), walletName.trim() + "." + persistenceType.getExtension());
|
||||||
|
if(unencrypted.exists()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getExistingWallet(String walletName) {
|
public static File getExistingWallet(String walletName) {
|
||||||
File encrypted = new File(getWalletsDir(), walletName);
|
File encrypted = new File(getWalletsDir(), walletName.trim());
|
||||||
File unencrypted = new File(getWalletsDir(), walletName + ".json");
|
|
||||||
|
|
||||||
if(encrypted.exists()) {
|
if(encrypted.exists()) {
|
||||||
return encrypted;
|
return encrypted;
|
||||||
} else if(unencrypted.exists()) {
|
}
|
||||||
|
|
||||||
|
for(PersistenceType persistenceType : PersistenceType.values()) {
|
||||||
|
File unencrypted = new File(getWalletsDir(), walletName.trim() + "." + persistenceType.getExtension());
|
||||||
|
if(unencrypted.exists()) {
|
||||||
return unencrypted;
|
return unencrypted;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -468,7 +314,7 @@ public class Storage {
|
||||||
return new File(System.getProperty("user.home"));
|
return new File(System.getProperty("user.home"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean createOwnerOnlyDirectory(File directory) {
|
public static boolean createOwnerOnlyDirectory(File directory) {
|
||||||
try {
|
try {
|
||||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||||
Files.createDirectories(directory.toPath());
|
Files.createDirectories(directory.toPath());
|
||||||
|
@ -519,162 +365,6 @@ public class Storage {
|
||||||
return ownerOnly;
|
return ownerOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ExtendedPublicKeySerializer implements JsonSerializer<ExtendedKey> {
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(ExtendedKey src, Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
return new JsonPrimitive(src.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ExtendedPublicKeyDeserializer implements JsonDeserializer<ExtendedKey> {
|
|
||||||
@Override
|
|
||||||
public ExtendedKey deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
||||||
return ExtendedKey.fromDescriptor(json.getAsJsonPrimitive().getAsString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ByteArraySerializer implements JsonSerializer<byte[]> {
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
return new JsonPrimitive(Utils.bytesToHex(src));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ByteArrayDeserializer implements JsonDeserializer<byte[]> {
|
|
||||||
@Override
|
|
||||||
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
||||||
return Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Sha256HashSerializer implements JsonSerializer<Sha256Hash> {
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(Sha256Hash src, Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
return new JsonPrimitive(src.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Sha256HashDeserializer implements JsonDeserializer<Sha256Hash> {
|
|
||||||
@Override
|
|
||||||
public Sha256Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
||||||
return Sha256Hash.wrap(json.getAsJsonPrimitive().getAsString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DateSerializer implements JsonSerializer<Date> {
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
return new JsonPrimitive(src.getTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DateDeserializer implements JsonDeserializer<Date> {
|
|
||||||
@Override
|
|
||||||
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
||||||
return new Date(json.getAsJsonPrimitive().getAsLong());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TransactionSerializer implements JsonSerializer<Transaction> {
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(Transaction src, Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
src.bitcoinSerializeToStream(baos);
|
|
||||||
return new JsonPrimitive(Utils.bytesToHex(baos.toByteArray()));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TransactionDeserializer implements JsonDeserializer<Transaction> {
|
|
||||||
@Override
|
|
||||||
public Transaction deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
||||||
byte[] rawTx = Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
|
||||||
return new Transaction(rawTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class KeystoreSerializer implements JsonSerializer<Keystore> {
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore);
|
|
||||||
if(keystore.hasPrivateKey()) {
|
|
||||||
jsonObject.remove("extendedPublicKey");
|
|
||||||
jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint");
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NodeSerializer implements JsonSerializer<WalletNode> {
|
|
||||||
@Override
|
|
||||||
public JsonElement serialize(WalletNode node, Type typeOfSrc, JsonSerializationContext context) {
|
|
||||||
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(node);
|
|
||||||
|
|
||||||
JsonArray children = jsonObject.getAsJsonArray("children");
|
|
||||||
Iterator<JsonElement> iter = children.iterator();
|
|
||||||
while(iter.hasNext()) {
|
|
||||||
JsonObject childObject = (JsonObject)iter.next();
|
|
||||||
removeEmptyCollection(childObject, "children");
|
|
||||||
removeEmptyCollection(childObject, "transactionOutputs");
|
|
||||||
|
|
||||||
if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("transactionOutputs") == null) {
|
|
||||||
iter.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeEmptyCollection(JsonObject jsonObject, String memberName) {
|
|
||||||
if(jsonObject.get(memberName) != null && jsonObject.getAsJsonArray(memberName).size() == 0) {
|
|
||||||
jsonObject.remove(memberName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NodeDeserializer implements JsonDeserializer<WalletNode> {
|
|
||||||
@Override
|
|
||||||
public WalletNode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
|
||||||
WalletNode node = getGson(false).fromJson(json, typeOfT);
|
|
||||||
node.parseDerivation();
|
|
||||||
|
|
||||||
for(WalletNode childNode : node.getChildren()) {
|
|
||||||
childNode.parseDerivation();
|
|
||||||
if(childNode.getChildren() == null) {
|
|
||||||
childNode.setChildren(new TreeSet<>());
|
|
||||||
}
|
|
||||||
if(childNode.getTransactionOutputs() == null) {
|
|
||||||
childNode.setTransactionOutputs(new TreeSet<>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 WalletBackupAndKey extends WalletAndKey {
|
|
||||||
public final Wallet backupWallet;
|
|
||||||
|
|
||||||
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, Key key) {
|
|
||||||
super(wallet, key);
|
|
||||||
this.backupWallet = backupWallet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LoadWalletService extends Service<WalletBackupAndKey> {
|
public static class LoadWalletService extends Service<WalletBackupAndKey> {
|
||||||
private final Storage storage;
|
private final Storage storage;
|
||||||
private final SecureString password;
|
private final SecureString password;
|
||||||
|
@ -688,7 +378,7 @@ public class Storage {
|
||||||
protected Task<WalletBackupAndKey> createTask() {
|
protected Task<WalletBackupAndKey> createTask() {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
protected WalletBackupAndKey call() throws IOException, StorageException {
|
protected WalletBackupAndKey call() throws IOException, StorageException {
|
||||||
WalletBackupAndKey walletBackupAndKey = storage.loadWallet(password);
|
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
|
||||||
password.clear();
|
password.clear();
|
||||||
return walletBackupAndKey;
|
return walletBackupAndKey;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class WalletBackupAndKey {
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final Wallet backupWallet;
|
||||||
|
private final ECKey encryptionKey;
|
||||||
|
private final Key key;
|
||||||
|
private final Map<Storage, WalletBackupAndKey> childWallets;
|
||||||
|
|
||||||
|
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<Storage, WalletBackupAndKey> childWallets) {
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.backupWallet = backupWallet;
|
||||||
|
this.encryptionKey = encryptionKey;
|
||||||
|
this.key = encryptionKey == null ? null : new Key(encryptionKey.getPrivKeyBytes(), keyDeriver.getSalt(), EncryptionType.Deriver.ARGON2);
|
||||||
|
this.childWallets = childWallets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getBackupWallet() {
|
||||||
|
return backupWallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ECKey getEncryptionKey() {
|
||||||
|
return encryptionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Key getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Storage, WalletBackupAndKey> getChildWallets() {
|
||||||
|
return childWallets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
if(encryptionKey != null) {
|
||||||
|
encryptionKey.clear();
|
||||||
|
}
|
||||||
|
if(key != null) {
|
||||||
|
key.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,7 @@ public class WalletForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() throws IOException {
|
public void save() throws IOException {
|
||||||
storage.storeWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveAndRefresh() throws IOException {
|
public void saveAndRefresh() throws IOException {
|
||||||
|
@ -145,7 +145,7 @@ public class WalletForm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.deleteBackups(Storage.TEMP_BACKUP_EXTENSION);
|
storage.deleteTempBackups();
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
@ -15,17 +16,17 @@ public class StorageTest extends IoTest {
|
||||||
@Test
|
@Test
|
||||||
public void loadWallet() throws IOException, MnemonicException, StorageException {
|
public void loadWallet() throws IOException, MnemonicException, StorageException {
|
||||||
Storage storage = new Storage(getFile("sparrow-single-wallet"));
|
Storage storage = new Storage(getFile("sparrow-single-wallet"));
|
||||||
Wallet wallet = storage.loadWallet("pass").wallet;
|
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
|
||||||
Assert.assertTrue(wallet.isValid());
|
Assert.assertTrue(wallet.isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loadSeedWallet() throws IOException, MnemonicException, StorageException {
|
public void loadSeedWallet() throws IOException, MnemonicException, StorageException {
|
||||||
Storage storage = new Storage(getFile("sparrow-single-seed-wallet"));
|
Storage storage = new Storage(getFile("sparrow-single-seed-wallet"));
|
||||||
Storage.WalletAndKey walletAndKey = storage.loadWallet("pass");
|
WalletBackupAndKey walletAndKey = storage.loadEncryptedWallet("pass");
|
||||||
Wallet wallet = walletAndKey.wallet;
|
Wallet wallet = walletAndKey.getWallet();
|
||||||
Wallet copy = wallet.copy();
|
Wallet copy = wallet.copy();
|
||||||
copy.decrypt(walletAndKey.key);
|
copy.decrypt(walletAndKey.getKey());
|
||||||
|
|
||||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||||
Keystore keystore = wallet.getKeystores().get(i);
|
Keystore keystore = wallet.getKeystores().get(i);
|
||||||
|
@ -51,12 +52,20 @@ public class StorageTest extends IoTest {
|
||||||
Assert.assertEquals("xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
|
Assert.assertEquals("xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
|
||||||
Assert.assertEquals("af6ebd81714c301c3a71fe11a7a9c99ccef4b33d4b36582220767bfa92768a2aa040f88b015b2465f8075a8b9dbf892a7d6e6c49932109f2cbc05ba0bd7f355fbcc34c237f71be5fb4dd7f8184e44cb0", 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().getMnemonicCode());
|
Assert.assertNull(wallet.getKeystores().get(0).getSeed().getMnemonicCode());
|
||||||
|
Assert.assertEquals("bc1q2mkrttcuzryrdyn9vtu3nfnt3jlngwn476ktus", wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleLoadTest() throws IOException, MnemonicException, StorageException {
|
||||||
|
for(int i = 0; i < 100; i++) {
|
||||||
|
loadSeedWallet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void saveWallet() throws IOException, MnemonicException, StorageException {
|
public void saveWallet() throws IOException, MnemonicException, StorageException {
|
||||||
Storage storage = new Storage(getFile("sparrow-single-wallet"));
|
Storage storage = new Storage(getFile("sparrow-single-wallet"));
|
||||||
Wallet wallet = storage.loadWallet("pass").wallet;
|
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
|
||||||
Assert.assertTrue(wallet.isValid());
|
Assert.assertTrue(wallet.isValid());
|
||||||
|
|
||||||
File tempWallet = File.createTempFile("sparrow", "tmp");
|
File tempWallet = File.createTempFile("sparrow", "tmp");
|
||||||
|
@ -65,10 +74,10 @@ public class StorageTest extends IoTest {
|
||||||
Storage tempStorage = new Storage(tempWallet);
|
Storage tempStorage = new Storage(tempWallet);
|
||||||
tempStorage.setKeyDeriver(storage.getKeyDeriver());
|
tempStorage.setKeyDeriver(storage.getKeyDeriver());
|
||||||
tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey());
|
tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey());
|
||||||
tempStorage.storeWallet(wallet);
|
tempStorage.saveWallet(wallet);
|
||||||
|
|
||||||
Storage temp2Storage = new Storage(tempWallet);
|
Storage temp2Storage = new Storage(tempWallet);
|
||||||
wallet = temp2Storage.loadWallet("pass").wallet;
|
wallet = temp2Storage.loadEncryptedWallet("pass").getWallet();
|
||||||
Assert.assertTrue(wallet.isValid());
|
Assert.assertTrue(wallet.isValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue