mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 13:26:44 +00:00
refactor storage to handle different persistence strategies
This commit is contained in:
parent
8bc8bdb2f2
commit
fb72010bdf
11 changed files with 611 additions and 400 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit cc32285d58fcd1120b27797ff1d882ae4ae8a1ed
|
||||
Subproject commit 7dca0d0c39404bd89a5a4589a79ffe0f688e8181
|
|
@ -740,13 +740,8 @@ public class AppController implements Initializable {
|
|||
Storage storage = new Storage(file);
|
||||
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<SecureString> 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<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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -825,7 +825,7 @@ public class AppServices {
|
|||
|
||||
Platform.runLater(() -> {
|
||||
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());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
412
src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java
Normal file
412
src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java
Normal file
|
@ -0,0 +1,412 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS;
|
||||
|
||||
public class JsonPersistence implements Persistence {
|
||||
public static final String HEADER_MAGIC_1 = "SPRW1";
|
||||
public static final int BINARY_HEADER_LENGTH = 28;
|
||||
|
||||
private final Gson gson;
|
||||
private AsymmetricKeyDeriver keyDeriver;
|
||||
|
||||
public JsonPersistence() {
|
||||
this.gson = getGson();
|
||||
}
|
||||
|
||||
public Wallet loadWallet(File jsonFile) throws IOException {
|
||||
try(Reader reader = new FileReader(jsonFile)) {
|
||||
return gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
}
|
||||
|
||||
public WalletBackupAndKey loadWallet(File walletFile, CharSequence password) throws IOException, StorageException {
|
||||
Wallet wallet;
|
||||
ECKey encryptionKey;
|
||||
|
||||
try(InputStream fileStream = new FileInputStream(walletFile)) {
|
||||
encryptionKey = getEncryptionKey(password, fileStream, null);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
|
||||
return new WalletBackupAndKey(wallet, null, encryptionKey, keyDeriver, null);
|
||||
}
|
||||
|
||||
public Map<File, Wallet> loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException {
|
||||
Map<File, Wallet> walletsMap = new LinkedHashMap<>();
|
||||
for(File file : walletFiles) {
|
||||
if(encryptionKey != null) {
|
||||
try(InputStream fileStream = new FileInputStream(file)) {
|
||||
encryptionKey = getEncryptionKey(null, fileStream, encryptionKey);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
walletsMap.put(file, gson.fromJson(reader, Wallet.class));
|
||||
}
|
||||
} else {
|
||||
walletsMap.put(file, loadWallet(file));
|
||||
}
|
||||
}
|
||||
|
||||
return walletsMap;
|
||||
}
|
||||
|
||||
public Map<Storage, WalletBackupAndKey> loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
File[] walletFiles = getChildWalletFiles(walletFile, masterWallet);
|
||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
Map<File, Wallet> loadedWallets = loadWallets(walletFiles, encryptionKey);
|
||||
for(Map.Entry<File, Wallet> entry : loadedWallets.entrySet()) {
|
||||
Storage storage = new Storage(entry.getKey());
|
||||
storage.setEncryptionPubKey(ECKey.fromPublicOnly(encryptionKey));
|
||||
storage.setKeyDeriver(getKeyDeriver());
|
||||
Wallet childWallet = entry.getValue();
|
||||
childWallet.setMasterWallet(masterWallet);
|
||||
childWallets.put(storage, new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
private File[] getChildWalletFiles(File walletFile, Wallet masterWallet) {
|
||||
File childDir = new File(walletFile.getParentFile(), masterWallet.getName() + "-child");
|
||||
if(!childDir.exists()) {
|
||||
return new File[0];
|
||||
}
|
||||
|
||||
File[] childFiles = childDir.listFiles(pathname -> {
|
||||
FileType fileType = IOUtils.getFileType(pathname);
|
||||
return pathname.getName().startsWith(masterWallet.getName()) && (fileType == FileType.BINARY || fileType == FileType.JSON);
|
||||
});
|
||||
|
||||
return childFiles == null ? new File[0] : childFiles;
|
||||
}
|
||||
|
||||
public File storeWallet(File walletFile, Wallet wallet) throws IOException {
|
||||
File parent = walletFile.getParentFile();
|
||||
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||
throw new IOException("Could not create folder " + parent);
|
||||
}
|
||||
|
||||
if(!walletFile.getName().endsWith(".json")) {
|
||||
File jsonFile = new File(parent, walletFile.getName() + ".json");
|
||||
if(walletFile.exists()) {
|
||||
if(!walletFile.renameTo(jsonFile)) {
|
||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
|
||||
}
|
||||
}
|
||||
walletFile = jsonFile;
|
||||
}
|
||||
|
||||
if(!walletFile.exists()) {
|
||||
Storage.createOwnerOnlyFile(walletFile);
|
||||
}
|
||||
|
||||
try(Writer writer = new FileWriter(walletFile)) {
|
||||
gson.toJson(wallet, writer);
|
||||
}
|
||||
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
public File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||
File parent = walletFile.getParentFile();
|
||||
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||
throw new IOException("Could not create folder " + parent);
|
||||
}
|
||||
|
||||
if(walletFile.getName().endsWith(".json")) {
|
||||
File noJsonFile = new File(parent, walletFile.getName().substring(0, walletFile.getName().lastIndexOf('.')));
|
||||
if(walletFile.exists()) {
|
||||
if(!walletFile.renameTo(noJsonFile)) {
|
||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
|
||||
}
|
||||
}
|
||||
walletFile = noJsonFile;
|
||||
}
|
||||
|
||||
if(!walletFile.exists()) {
|
||||
Storage.createOwnerOnlyFile(walletFile);
|
||||
}
|
||||
|
||||
try(OutputStream outputStream = new FileOutputStream(walletFile)) {
|
||||
writeBinaryHeader(outputStream);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(new DeflaterOutputStream(new ECIESOutputStream(outputStream, encryptionPubKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
gson.toJson(wallet, writer);
|
||||
//Close the writer explicitly as the try-resources block will not do so
|
||||
writer.close();
|
||||
}
|
||||
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
private void writeBinaryHeader(OutputStream outputStream) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(21);
|
||||
buf.put(HEADER_MAGIC_1.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put(keyDeriver.getSalt());
|
||||
|
||||
byte[] encoded = Base64.getEncoder().encode(buf.array());
|
||||
if(encoded.length != BINARY_HEADER_LENGTH) {
|
||||
throw new IllegalStateException("Header length not " + BINARY_HEADER_LENGTH + " bytes");
|
||||
}
|
||||
outputStream.write(encoded);
|
||||
}
|
||||
|
||||
private static byte[] getEncryptionMagic() {
|
||||
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
}
|
||||
|
||||
private ECKey getEncryptionKey(CharSequence password, InputStream inputStream, ECKey alreadyDerivedKey) throws IOException, StorageException {
|
||||
if(password != null && password.equals("")) {
|
||||
return Storage.NO_PASSWORD_KEY;
|
||||
}
|
||||
|
||||
AsymmetricKeyDeriver keyDeriver = getKeyDeriver(inputStream);
|
||||
return alreadyDerivedKey == null ? keyDeriver.deriveECKey(password) : alreadyDerivedKey;
|
||||
}
|
||||
|
||||
public AsymmetricKeyDeriver getKeyDeriver() {
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
||||
this.keyDeriver = keyDeriver;
|
||||
}
|
||||
|
||||
private AsymmetricKeyDeriver getKeyDeriver(InputStream inputStream) throws IOException, StorageException {
|
||||
if(keyDeriver == null) {
|
||||
keyDeriver = getWalletKeyDeriver(inputStream);
|
||||
} else if(inputStream != null) {
|
||||
inputStream.skip(BINARY_HEADER_LENGTH);
|
||||
}
|
||||
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
private AsymmetricKeyDeriver getWalletKeyDeriver(InputStream inputStream) throws IOException, StorageException {
|
||||
byte[] salt = new byte[SPRW1_PARAMETERS.saltLength];
|
||||
|
||||
if(inputStream != null) {
|
||||
byte[] header = new byte[BINARY_HEADER_LENGTH];
|
||||
int read = inputStream.read(header);
|
||||
if(read != BINARY_HEADER_LENGTH) {
|
||||
throw new StorageException("Not a Sparrow wallet - invalid header");
|
||||
}
|
||||
try {
|
||||
byte[] decodedHeader = Base64.getDecoder().decode(header);
|
||||
byte[] magic = Arrays.copyOfRange(decodedHeader, 0, HEADER_MAGIC_1.length());
|
||||
if(!HEADER_MAGIC_1.equals(new String(magic, StandardCharsets.UTF_8))) {
|
||||
throw new StorageException("Not a Sparrow wallet - invalid magic");
|
||||
}
|
||||
salt = Arrays.copyOfRange(decodedHeader, HEADER_MAGIC_1.length(), decodedHeader.length);
|
||||
} catch(IllegalArgumentException e) {
|
||||
throw new StorageException("Not a Sparrow wallet - invalid header");
|
||||
}
|
||||
} else {
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
secureRandom.nextBytes(salt);
|
||||
}
|
||||
|
||||
return new Argon2KeyDeriver(salt);
|
||||
}
|
||||
|
||||
public PersistenceType getType() {
|
||||
return PersistenceType.JSON;
|
||||
}
|
||||
|
||||
public static Gson getGson() {
|
||||
return getGson(true);
|
||||
}
|
||||
|
||||
private static Gson getGson(boolean includeWalletSerializers) {
|
||||
GsonBuilder gsonBuilder = new GsonBuilder();
|
||||
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeySerializer());
|
||||
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeyDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArraySerializer());
|
||||
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArrayDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Date.class, new DateSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionDeserializer());
|
||||
if(includeWalletSerializers) {
|
||||
gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeDeserializer());
|
||||
}
|
||||
|
||||
gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes field) {
|
||||
return field.getDeclaringClass() == Wallet.class && field.getName().equals("masterWallet");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> clazz) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
|
||||
}
|
||||
|
||||
private static class ExtendedPublicKeySerializer implements JsonSerializer<ExtendedKey> {
|
||||
@Override
|
||||
public JsonElement serialize(ExtendedKey src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExtendedPublicKeyDeserializer implements JsonDeserializer<ExtendedKey> {
|
||||
@Override
|
||||
public ExtendedKey deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return ExtendedKey.fromDescriptor(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArraySerializer implements JsonSerializer<byte[]> {
|
||||
@Override
|
||||
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(Utils.bytesToHex(src));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArrayDeserializer implements JsonDeserializer<byte[]> {
|
||||
@Override
|
||||
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256HashSerializer implements JsonSerializer<Sha256Hash> {
|
||||
@Override
|
||||
public JsonElement serialize(Sha256Hash src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256HashDeserializer implements JsonDeserializer<Sha256Hash> {
|
||||
@Override
|
||||
public Sha256Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return Sha256Hash.wrap(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DateSerializer implements JsonSerializer<Date> {
|
||||
@Override
|
||||
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DateDeserializer implements JsonDeserializer<Date> {
|
||||
@Override
|
||||
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return new Date(json.getAsJsonPrimitive().getAsLong());
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionSerializer implements JsonSerializer<Transaction> {
|
||||
@Override
|
||||
public JsonElement serialize(Transaction src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
src.bitcoinSerializeToStream(baos);
|
||||
return new JsonPrimitive(Utils.bytesToHex(baos.toByteArray()));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionDeserializer implements JsonDeserializer<Transaction> {
|
||||
@Override
|
||||
public Transaction deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
byte[] rawTx = Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
||||
return new Transaction(rawTx);
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeystoreSerializer implements JsonSerializer<Keystore> {
|
||||
@Override
|
||||
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore);
|
||||
if(keystore.hasPrivateKey()) {
|
||||
jsonObject.remove("extendedPublicKey");
|
||||
jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint");
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NodeSerializer implements JsonSerializer<WalletNode> {
|
||||
@Override
|
||||
public JsonElement serialize(WalletNode node, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(node);
|
||||
|
||||
JsonArray children = jsonObject.getAsJsonArray("children");
|
||||
Iterator<JsonElement> iter = children.iterator();
|
||||
while(iter.hasNext()) {
|
||||
JsonObject childObject = (JsonObject)iter.next();
|
||||
removeEmptyCollection(childObject, "children");
|
||||
removeEmptyCollection(childObject, "transactionOutputs");
|
||||
|
||||
if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("transactionOutputs") == null) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
private void removeEmptyCollection(JsonObject jsonObject, String memberName) {
|
||||
if(jsonObject.get(memberName) != null && jsonObject.getAsJsonArray(memberName).size() == 0) {
|
||||
jsonObject.remove(memberName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NodeDeserializer implements JsonDeserializer<WalletNode> {
|
||||
@Override
|
||||
public WalletNode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
WalletNode node = getGson(false).fromJson(json, typeOfT);
|
||||
node.parseDerivation();
|
||||
|
||||
for(WalletNode childNode : node.getChildren()) {
|
||||
childNode.parseDerivation();
|
||||
if(childNode.getChildren() == null) {
|
||||
childNode.setChildren(new TreeSet<>());
|
||||
}
|
||||
if(childNode.getTransactionOutputs() == null) {
|
||||
childNode.setTransactionOutputs(new TreeSet<>());
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
22
src/main/java/com/sparrowwallet/sparrow/io/Persistence.java
Normal file
22
src/main/java/com/sparrowwallet/sparrow/io/Persistence.java
Normal file
|
@ -0,0 +1,22 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
public interface Persistence {
|
||||
Wallet loadWallet(File walletFile) throws IOException;
|
||||
WalletBackupAndKey loadWallet(File walletFile, CharSequence password) throws IOException, StorageException;
|
||||
Map<File, Wallet> loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException;
|
||||
Map<Storage, WalletBackupAndKey> loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException;
|
||||
File storeWallet(File walletFile, Wallet wallet) throws IOException;
|
||||
File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException;
|
||||
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
|
||||
AsymmetricKeyDeriver getKeyDeriver();
|
||||
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
||||
PersistenceType getType();
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
public enum PersistenceType {
|
||||
JSON("json");
|
||||
|
||||
private final String name;
|
||||
|
||||
private PersistenceType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return getName();
|
||||
}
|
||||
}
|
|
@ -1,16 +1,10 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
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<Storage, WalletBackupAndKey> 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<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 {
|
||||
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<File, Wallet> 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<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> {
|
||||
private final Storage storage;
|
||||
private final SecureString password;
|
||||
|
@ -688,7 +378,7 @@ public class Storage {
|
|||
protected Task<WalletBackupAndKey> createTask() {
|
||||
return new Task<>() {
|
||||
protected WalletBackupAndKey call() throws IOException, StorageException {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadWallet(password);
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
|
||||
password.clear();
|
||||
return walletBackupAndKey;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class WalletBackupAndKey {
|
||||
private final Wallet wallet;
|
||||
private final Wallet backupWallet;
|
||||
private final ECKey encryptionKey;
|
||||
private final Key key;
|
||||
private final Map<Storage, WalletBackupAndKey> childWallets;
|
||||
|
||||
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<Storage, WalletBackupAndKey> childWallets) {
|
||||
this.wallet = wallet;
|
||||
this.backupWallet = backupWallet;
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.key = encryptionKey == null ? null : new Key(encryptionKey.getPrivKeyBytes(), keyDeriver.getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
this.childWallets = childWallets;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public Wallet getBackupWallet() {
|
||||
return backupWallet;
|
||||
}
|
||||
|
||||
public ECKey getEncryptionKey() {
|
||||
return encryptionKey;
|
||||
}
|
||||
|
||||
public Key getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Map<Storage, WalletBackupAndKey> getChildWallets() {
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
if(encryptionKey != null) {
|
||||
encryptionKey.clear();
|
||||
}
|
||||
if(key != null) {
|
||||
key.clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -71,7 +71,7 @@ public class WalletForm {
|
|||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue