refactor storage to handle different persistence strategies

This commit is contained in:
Craig Raw 2021-05-14 12:55:12 +02:00
parent 8bc8bdb2f2
commit fb72010bdf
11 changed files with 611 additions and 400 deletions

2
drongo

@ -1 +1 @@
Subproject commit cc32285d58fcd1120b27797ff1d882ae4ae8a1ed Subproject commit 7dca0d0c39404bd89a5a4589a79ffe0f688e8181

View file

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

View file

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

View file

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

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

View 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();
}

View file

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

View file

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

View file

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

View file

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

View file

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