From fb72010bdf5f0e21808d0e6831cb93181341311b Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 14 May 2021 12:55:12 +0200 Subject: [PATCH] refactor storage to handle different persistence strategies --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 50 ++- .../sparrowwallet/sparrow/AppServices.java | 2 +- .../sparrow/io/ColdcardMultisig.java | 2 +- .../sparrow/io/JsonPersistence.java | 412 +++++++++++++++++ .../sparrowwallet/sparrow/io/Persistence.java | 22 + .../sparrow/io/PersistenceType.java | 19 + .../com/sparrowwallet/sparrow/io/Storage.java | 424 +++--------------- .../sparrow/io/WalletBackupAndKey.java | 51 +++ .../sparrow/wallet/WalletForm.java | 4 +- .../sparrowwallet/sparrow/io/StorageTest.java | 23 +- 11 files changed, 611 insertions(+), 400 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/Persistence.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/PersistenceType.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java diff --git a/drongo b/drongo index cc32285d..7dca0d0c 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit cc32285d58fcd1120b27797ff1d882ae4ae8a1ed +Subproject commit 7dca0d0c39404bd89a5a4589a79ffe0f688e8181 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 3a45ba0c..790cfa7b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -740,13 +740,8 @@ public class AppController implements Initializable { Storage storage = new Storage(file); FileType fileType = IOUtils.getFileType(file); if(FileType.JSON.equals(fileType)) { - Storage.WalletBackupAndKey walletBackupAndKey = storage.loadWallet(); - checkWalletNetwork(walletBackupAndKey.wallet); - restorePublicKeysFromSeed(walletBackupAndKey.wallet, null); - if(!walletBackupAndKey.wallet.isValid()) { - throw new IllegalStateException("Wallet file is not valid."); - } - addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow); + WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet(); + openWallet(storage, walletBackupAndKey, this, forceSameWindow); } else if(FileType.BINARY.equals(fileType)) { WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); Optional optionalPassword = dlg.showAndWait(); @@ -758,16 +753,8 @@ public class AppController implements Initializable { Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password); loadWalletService.setOnSucceeded(workerStateEvent -> { EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done")); - Storage.WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue(); - try { - 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(); - } + WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue(); + openWallet(storage, walletBackupAndKey, this, forceSameWindow); }); loadWalletService.setOnFailed(workerStateEvent -> { 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 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) { 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."); @@ -937,7 +943,7 @@ public class AppController implements Initializable { if(password.get().length() == 0) { try { storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY); - storage.storeWallet(wallet); + storage.saveWallet(wallet); addWalletTabOrWindow(storage, wallet, null, false); } catch(IOException 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); wallet.encrypt(key); storage.setEncryptionPubKey(encryptionPubKey); - storage.storeWallet(wallet); + storage.saveWallet(wallet); addWalletTabOrWindow(storage, wallet, null, false); } catch(IOException 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); if(existingWalletWindow instanceof Stage) { Stage existingWalletStage = (Stage)existingWalletWindow; existingWalletStage.toFront(); EventManager.get().post(new ViewWalletEvent(existingWalletWindow, wallet, storage)); - return; + return this; } if(!forceSameWindow && Config.get().isOpenWalletsInNewWindows() && !getOpenWallets().isEmpty()) { @@ -1048,8 +1054,10 @@ public class AppController implements Initializable { stage.toFront(); stage.setX(AppServices.get().getWalletWindowMaxX() + 30); appController.addWalletTab(storage, wallet, backupWallet); + return appController; } else { addWalletTab(storage, wallet, backupWallet); + return this; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 8c5d3fe0..580fea57 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -825,7 +825,7 @@ public class AppServices { Platform.runLater(() -> { if(!Window.getWindows().isEmpty()) { - List walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList()); + List 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()); } }); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java index b993dba4..9e2047e1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java @@ -36,7 +36,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle @Override public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { 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.setSource(KeystoreSource.HW_AIRGAPPED); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java new file mode 100644 index 00000000..f86e11bd --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java @@ -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 loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException { + Map 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 loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException { + File[] walletFiles = getChildWalletFiles(walletFile, masterWallet); + Map childWallets = new LinkedHashMap<>(); + Map loadedWallets = loadWallets(walletFiles, encryptionKey); + for(Map.Entry 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 { + @Override + public JsonElement serialize(ExtendedKey src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } + } + + private static class ExtendedPublicKeyDeserializer implements JsonDeserializer { + @Override + public ExtendedKey deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return ExtendedKey.fromDescriptor(json.getAsJsonPrimitive().getAsString()); + } + } + + private static class ByteArraySerializer implements JsonSerializer { + @Override + public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(Utils.bytesToHex(src)); + } + } + + private static class ByteArrayDeserializer implements JsonDeserializer { + @Override + public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Utils.hexToBytes(json.getAsJsonPrimitive().getAsString()); + } + } + + private static class Sha256HashSerializer implements JsonSerializer { + @Override + public JsonElement serialize(Sha256Hash src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } + } + + private static class Sha256HashDeserializer implements JsonDeserializer { + @Override + public Sha256Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Sha256Hash.wrap(json.getAsJsonPrimitive().getAsString()); + } + } + + private static class DateSerializer implements JsonSerializer { + @Override + public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getTime()); + } + } + + private static class DateDeserializer implements JsonDeserializer { + @Override + public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new Date(json.getAsJsonPrimitive().getAsLong()); + } + } + + private static class TransactionSerializer implements JsonSerializer { + @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 { + @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 { + @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 { + @Override + public JsonElement serialize(WalletNode node, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(node); + + JsonArray children = jsonObject.getAsJsonArray("children"); + Iterator 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 { + @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; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Persistence.java b/src/main/java/com/sparrowwallet/sparrow/io/Persistence.java new file mode 100644 index 00000000..9ba33513 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/Persistence.java @@ -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 loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException; + Map 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(); +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/PersistenceType.java b/src/main/java/com/sparrowwallet/sparrow/io/PersistenceType.java new file mode 100644 index 00000000..90658beb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/PersistenceType.java @@ -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(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index c2a966b9..1013e023 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -1,16 +1,10 @@ package com.sparrowwallet.sparrow.io; -import com.google.gson.*; -import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.Utils; 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.WalletNode; import com.sparrowwallet.sparrow.MainApp; import javafx.concurrent.Service; import javafx.concurrent.Task; @@ -19,13 +13,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; -import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.text.DateFormat; @@ -33,9 +23,6 @@ import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.*; - -import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS; public class Storage { 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_BACKUP_DIR = "backup"; 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"; + private final Persistence persistence; private File walletFile; - private final Gson gson; - private AsymmetricKeyDeriver keyDeriver; private ECKey encryptionPubKey; public Storage(File walletFile) { + this.persistence = new JsonPersistence(); this.walletFile = walletFile; - this.gson = getGson(); } public File getWalletFile() { return walletFile; } - 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()); - } - - 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); - } - } + public WalletBackupAndKey loadUnencryptedWallet() throws IOException, StorageException { + Wallet wallet = persistence.loadWallet(walletFile); + Wallet backupWallet = loadBackupWallet(null); + Map childWallets = persistence.loadChildWallets(walletFile, wallet, null); 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 { - Reader reader = new FileReader(jsonFile); - Wallet wallet = gson.fromJson(reader, Wallet.class); - reader.close(); + public WalletBackupAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException { + WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(walletFile, password); + Wallet backupWallet = loadBackupWallet(masterWalletAndKey.getEncryptionKey()); + Map 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 { - WalletAndKey walletAndKey = loadWallet(walletFile, password); - - WalletAndKey backupAndKey = new WalletAndKey(null, null); - File[] backups = getBackups(TEMP_BACKUP_EXTENSION, "json." + TEMP_BACKUP_EXTENSION); - if(backups.length > 0) { - try { - backupAndKey = loadWallet(backups[0], password); - } catch(Exception e) { - log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e); - } + protected Wallet loadBackupWallet(ECKey encryptionKey) throws IOException, StorageException { + Map backupWallets; + if(encryptionKey != null) { + File[] backups = getBackups(TEMP_BACKUP_EXTENSION, persistence.getType().getExtension() + "." + TEMP_BACKUP_EXTENSION); + backupWallets = persistence.loadWallets(backups, encryptionKey); + return backupWallets.isEmpty() ? null : backupWallets.values().iterator().next(); + } else { + File[] backups = getBackups(persistence.getType().getExtension() + "." + TEMP_BACKUP_EXTENSION); + backupWallets = persistence.loadWallets(backups, null); } - 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 { - 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 { + public void saveWallet(Wallet wallet) throws IOException { if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) { - storeWallet(encryptionPubKey, wallet); + walletFile = persistence.storeWallet(walletFile, wallet, encryptionPubKey); return; } - File parent = walletFile.getParentFile(); - 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); + walletFile = persistence.storeWallet(walletFile, wallet); } 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(); Date backupDate = new Date(); @@ -260,7 +134,11 @@ public class Storage { deleteBackups(null); } - public void deleteBackups(String extension) { + public void deleteTempBackups() { + deleteBackups(Storage.TEMP_BACKUP_EXTENSION); + } + + private void deleteBackups(String extension) { File[] backups = getBackups(extension); for(File backup : backups) { backup.delete(); @@ -280,6 +158,7 @@ public class Storage { (notExtension == null || !name.endsWith("." + notExtension)); }); + backups = backups == null ? new File[0] : backups; Arrays.sort(backups, Comparator.comparing(o -> getBackupDate(((File)o).getName())).reversed()); return backups; @@ -303,77 +182,44 @@ public class Storage { } public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException { - return getEncryptionKey(password, null); - } - - private ECKey getEncryptionKey(CharSequence password, InputStream inputStream) throws IOException, StorageException { - if(password.equals("")) { - return NO_PASSWORD_KEY; - } - - return getKeyDeriver(inputStream).deriveECKey(password); + return persistence.getEncryptionKey(password); } public AsymmetricKeyDeriver getKeyDeriver() { - return keyDeriver; + return persistence.getKeyDeriver(); } void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) { - this.keyDeriver = keyDeriver; - } - - private AsymmetricKeyDeriver getKeyDeriver(InputStream inputStream) throws IOException, StorageException { - if(keyDeriver == null) { - byte[] salt = new byte[SPRW1_PARAMETERS.saltLength]; - - if(inputStream != null) { - byte[] header = new byte[BINARY_HEADER_LENGTH]; - int read = inputStream.read(header); - if(read != BINARY_HEADER_LENGTH) { - throw new StorageException("Not a Sparrow wallet - invalid header"); - } - 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); + persistence.setKeyDeriver(keyDeriver); } public static boolean walletExists(String walletName) { 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) { - File encrypted = new File(getWalletsDir(), walletName); - File unencrypted = new File(getWalletsDir(), walletName + ".json"); - + File encrypted = new File(getWalletsDir(), walletName.trim()); if(encrypted.exists()) { return encrypted; - } else if(unencrypted.exists()) { - return unencrypted; + } + + for(PersistenceType persistenceType : PersistenceType.values()) { + File unencrypted = new File(getWalletsDir(), walletName.trim() + "." + persistenceType.getExtension()); + if(unencrypted.exists()) { + return unencrypted; + } } return null; @@ -468,7 +314,7 @@ public class Storage { return new File(System.getProperty("user.home")); } - private static boolean createOwnerOnlyDirectory(File directory) { + public static boolean createOwnerOnlyDirectory(File directory) { try { if(Platform.getCurrent() == Platform.WINDOWS) { Files.createDirectories(directory.toPath()); @@ -519,162 +365,6 @@ public class Storage { return ownerOnly; } - private static class ExtendedPublicKeySerializer implements JsonSerializer { - @Override - public JsonElement serialize(ExtendedKey src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.toString()); - } - } - - private static class ExtendedPublicKeyDeserializer implements JsonDeserializer { - @Override - public ExtendedKey deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return ExtendedKey.fromDescriptor(json.getAsJsonPrimitive().getAsString()); - } - } - - private static class ByteArraySerializer implements JsonSerializer { - @Override - public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(Utils.bytesToHex(src)); - } - } - - private static class ByteArrayDeserializer implements JsonDeserializer { - @Override - public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return Utils.hexToBytes(json.getAsJsonPrimitive().getAsString()); - } - } - - private static class Sha256HashSerializer implements JsonSerializer { - @Override - public JsonElement serialize(Sha256Hash src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.toString()); - } - } - - private static class Sha256HashDeserializer implements JsonDeserializer { - @Override - public Sha256Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return Sha256Hash.wrap(json.getAsJsonPrimitive().getAsString()); - } - } - - private static class DateSerializer implements JsonSerializer { - @Override - public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.getTime()); - } - } - - private static class DateDeserializer implements JsonDeserializer { - @Override - public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return new Date(json.getAsJsonPrimitive().getAsLong()); - } - } - - private static class TransactionSerializer implements JsonSerializer { - @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 { - @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 { - @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 { - @Override - public JsonElement serialize(WalletNode node, Type typeOfSrc, JsonSerializationContext context) { - JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(node); - - JsonArray children = jsonObject.getAsJsonArray("children"); - Iterator 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 { - @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 { private final Storage storage; private final SecureString password; @@ -688,7 +378,7 @@ public class Storage { protected Task createTask() { return new Task<>() { protected WalletBackupAndKey call() throws IOException, StorageException { - WalletBackupAndKey walletBackupAndKey = storage.loadWallet(password); + WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password); password.clear(); return walletBackupAndKey; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java b/src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java new file mode 100644 index 00000000..755d4d01 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/WalletBackupAndKey.java @@ -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 childWallets; + + public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map 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 getChildWallets() { + return childWallets; + } + + public void clear() { + if(encryptionKey != null) { + encryptionKey.clear(); + } + if(key != null) { + key.clear(); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index ce291f2c..da17b684 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -71,7 +71,7 @@ public class WalletForm { } public void save() throws IOException { - storage.storeWallet(wallet); + storage.saveWallet(wallet); } public void saveAndRefresh() throws IOException { @@ -145,7 +145,7 @@ public class WalletForm { } } - storage.deleteBackups(Storage.TEMP_BACKUP_EXTENSION); + storage.deleteTempBackups(); return changed; } diff --git a/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java b/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java index b5552dda..5b5656c0 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.io; +import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; @@ -15,17 +16,17 @@ public class StorageTest extends IoTest { @Test public void loadWallet() throws IOException, MnemonicException, StorageException { Storage storage = new Storage(getFile("sparrow-single-wallet")); - Wallet wallet = storage.loadWallet("pass").wallet; + Wallet wallet = storage.loadEncryptedWallet("pass").getWallet(); Assert.assertTrue(wallet.isValid()); } @Test public void loadSeedWallet() throws IOException, MnemonicException, StorageException { Storage storage = new Storage(getFile("sparrow-single-seed-wallet")); - Storage.WalletAndKey walletAndKey = storage.loadWallet("pass"); - Wallet wallet = walletAndKey.wallet; + WalletBackupAndKey walletAndKey = storage.loadEncryptedWallet("pass"); + Wallet wallet = walletAndKey.getWallet(); Wallet copy = wallet.copy(); - copy.decrypt(walletAndKey.key); + copy.decrypt(walletAndKey.getKey()); for(int i = 0; i < wallet.getKeystores().size(); 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("af6ebd81714c301c3a71fe11a7a9c99ccef4b33d4b36582220767bfa92768a2aa040f88b015b2465f8075a8b9dbf892a7d6e6c49932109f2cbc05ba0bd7f355fbcc34c237f71be5fb4dd7f8184e44cb0", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getEncryptedData().getEncryptedBytes())); 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 public void saveWallet() throws IOException, MnemonicException, StorageException { Storage storage = new Storage(getFile("sparrow-single-wallet")); - Wallet wallet = storage.loadWallet("pass").wallet; + Wallet wallet = storage.loadEncryptedWallet("pass").getWallet(); Assert.assertTrue(wallet.isValid()); File tempWallet = File.createTempFile("sparrow", "tmp"); @@ -65,10 +74,10 @@ public class StorageTest extends IoTest { Storage tempStorage = new Storage(tempWallet); tempStorage.setKeyDeriver(storage.getKeyDeriver()); tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey()); - tempStorage.storeWallet(wallet); + tempStorage.saveWallet(wallet); Storage temp2Storage = new Storage(tempWallet); - wallet = temp2Storage.loadWallet("pass").wallet; + wallet = temp2Storage.loadEncryptedWallet("pass").getWallet(); Assert.assertTrue(wallet.isValid()); } }