mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-22 20:19:21 +00:00
Encrypt persisted data using password protected symmetric key (#279)
This commit is contained in:
parent
75c66ee43f
commit
5b38eab716
13 changed files with 254 additions and 169 deletions
|
@ -218,9 +218,9 @@ public class Encryption {
|
||||||
|
|
||||||
public static SecretKey generateSecretKey(int bits) {
|
public static SecretKey generateSecretKey(int bits) {
|
||||||
try {
|
try {
|
||||||
KeyGenerator keyPairGenerator = KeyGenerator.getInstance(SYM_KEY_ALGO);
|
KeyGenerator keyGenerator = KeyGenerator.getInstance(SYM_KEY_ALGO);
|
||||||
keyPairGenerator.init(bits);
|
keyGenerator.init(bits);
|
||||||
return keyPairGenerator.generateKey();
|
return keyGenerator.generateKey();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
log.error("Couldn't generate key", e);
|
log.error("Couldn't generate key", e);
|
||||||
throw new RuntimeException("Couldn't generate key");
|
throw new RuntimeException("Couldn't generate key");
|
||||||
|
|
|
@ -20,6 +20,8 @@ package bisq.common.crypto;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
@ -36,13 +38,14 @@ public final class KeyRing {
|
||||||
|
|
||||||
private final KeyStorage keyStorage;
|
private final KeyStorage keyStorage;
|
||||||
|
|
||||||
|
private SecretKey symmetricKey;
|
||||||
private KeyPair signatureKeyPair;
|
private KeyPair signatureKeyPair;
|
||||||
private KeyPair encryptionKeyPair;
|
private KeyPair encryptionKeyPair;
|
||||||
private PubKeyRing pubKeyRing;
|
private PubKeyRing pubKeyRing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the KeyRing. Unlocks if not encrypted. Does not generate keys.
|
* Creates the KeyRing. Unlocks if not encrypted. Does not generate keys.
|
||||||
*
|
*
|
||||||
* @param keyStorage Persisted storage
|
* @param keyStorage Persisted storage
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -52,7 +55,7 @@ public final class KeyRing {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates KeyRing with a password. Attempts to generate keys if they don't exist.
|
* Creates KeyRing with a password. Attempts to generate keys if they don't exist.
|
||||||
*
|
*
|
||||||
* @param keyStorage Persisted storage
|
* @param keyStorage Persisted storage
|
||||||
* @param password The password to unlock the keys or to generate new keys, nullable.
|
* @param password The password to unlock the keys or to generate new keys, nullable.
|
||||||
* @param generateKeys Generate new keys with password if not created yet.
|
* @param generateKeys Generate new keys with password if not created yet.
|
||||||
|
@ -67,7 +70,8 @@ public final class KeyRing {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUnlocked() {
|
public boolean isUnlocked() {
|
||||||
boolean isUnlocked = this.signatureKeyPair != null
|
boolean isUnlocked = this.symmetricKey != null
|
||||||
|
&& this.signatureKeyPair != null
|
||||||
&& this.encryptionKeyPair != null
|
&& this.encryptionKeyPair != null
|
||||||
&& this.pubKeyRing != null;
|
&& this.pubKeyRing != null;
|
||||||
return isUnlocked;
|
return isUnlocked;
|
||||||
|
@ -80,21 +84,23 @@ public final class KeyRing {
|
||||||
public void lockKeys() {
|
public void lockKeys() {
|
||||||
signatureKeyPair = null;
|
signatureKeyPair = null;
|
||||||
encryptionKeyPair = null;
|
encryptionKeyPair = null;
|
||||||
|
symmetricKey = null;
|
||||||
pubKeyRing = null;
|
pubKeyRing = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unlocks the keyring with a given password if required. If the keyring is already
|
* Unlocks the keyring with a given password if required. If the keyring is already
|
||||||
* unlocked, do nothing.
|
* unlocked, do nothing.
|
||||||
*
|
*
|
||||||
* @param password Decrypts the or encrypts newly generated keys with the given password.
|
* @param password Decrypts the or encrypts newly generated keys with the given password.
|
||||||
* @return Whether KeyRing is unlocked
|
* @return Whether KeyRing is unlocked
|
||||||
*/
|
*/
|
||||||
public boolean unlockKeys(@Nullable String password, boolean generateKeys) throws IncorrectPasswordException {
|
public boolean unlockKeys(@Nullable String password, boolean generateKeys) throws IncorrectPasswordException {
|
||||||
if (isUnlocked()) return true;
|
if (isUnlocked()) return true;
|
||||||
if (keyStorage.allKeyFilesExist()) {
|
if (keyStorage.allKeyFilesExist()) {
|
||||||
signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE, password);
|
symmetricKey = keyStorage.loadSecretKey(KeyStorage.KeyEntry.SYM_ENCRYPTION, password);
|
||||||
encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION, password);
|
signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE, symmetricKey);
|
||||||
|
encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION, symmetricKey);
|
||||||
if (signatureKeyPair != null && encryptionKeyPair != null) pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
if (signatureKeyPair != null && encryptionKeyPair != null) pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
||||||
} else if (generateKeys) {
|
} else if (generateKeys) {
|
||||||
generateKeys(password);
|
generateKeys(password);
|
||||||
|
@ -104,22 +110,24 @@ public final class KeyRing {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new set of keys if the current keyring is closed.
|
* Generates a new set of keys if the current keyring is closed.
|
||||||
*
|
*
|
||||||
* @param password The password to unlock the keys or to generate new keys, nullable.
|
* @param password The password to unlock the keys or to generate new keys, nullable.
|
||||||
*/
|
*/
|
||||||
public void generateKeys(String password) {
|
public void generateKeys(String password) {
|
||||||
if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
|
if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
|
||||||
|
symmetricKey = Encryption.generateSecretKey(256);
|
||||||
signatureKeyPair = Sig.generateKeyPair();
|
signatureKeyPair = Sig.generateKeyPair();
|
||||||
encryptionKeyPair = Encryption.generateKeyPair();
|
encryptionKeyPair = Encryption.generateKeyPair();
|
||||||
pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
||||||
keyStorage.saveKeyRing(this, password);
|
keyStorage.saveKeyRing(this, null, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't print keys for security reasons
|
// Don't print keys for security reasons
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "KeyRing{" +
|
return "KeyRing{" +
|
||||||
"signatureKeyPair.hashCode()=" + signatureKeyPair.hashCode() +
|
"symmetricKey.hashCode()=" + symmetricKey.hashCode() +
|
||||||
|
", signatureKeyPair.hashCode()=" + signatureKeyPair.hashCode() +
|
||||||
", encryptionKeyPair.hashCode()=" + encryptionKeyPair.hashCode() +
|
", encryptionKeyPair.hashCode()=" + encryptionKeyPair.hashCode() +
|
||||||
", pubKeyRing.hashCode()=" + pubKeyRing.hashCode() +
|
", pubKeyRing.hashCode()=" + pubKeyRing.hashCode() +
|
||||||
'}';
|
'}';
|
||||||
|
|
|
@ -18,26 +18,22 @@
|
||||||
package bisq.common.crypto;
|
package bisq.common.crypto;
|
||||||
|
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
import bisq.common.file.FileUtil;
|
|
||||||
|
|
||||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyStore;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.security.UnrecoverableKeyException;
|
||||||
import java.security.interfaces.DSAParams;
|
import java.security.interfaces.DSAParams;
|
||||||
import java.security.interfaces.DSAPrivateKey;
|
import java.security.interfaces.DSAPrivateKey;
|
||||||
import java.security.interfaces.RSAPrivateCrtKey;
|
import java.security.interfaces.RSAPrivateCrtKey;
|
||||||
|
@ -47,9 +43,6 @@ import java.security.spec.KeySpec;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.security.spec.RSAPublicKeySpec;
|
import java.security.spec.RSAPublicKeySpec;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
@ -57,9 +50,6 @@ import java.io.IOException;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -67,25 +57,28 @@ import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import static bisq.common.util.Preconditions.checkDir;
|
import static bisq.common.util.Preconditions.checkDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyStorage uses password protection to save a symmetric key in PKCS#12 format.
|
||||||
|
* The symmetric key is used to encrypt and decrypt other keys in the key ring and other types of persistence.
|
||||||
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
public class KeyStorage {
|
public class KeyStorage {
|
||||||
private static final Logger log = LoggerFactory.getLogger(KeyStorage.class);
|
|
||||||
private static final int SALT_LENGTH = 20;
|
|
||||||
|
|
||||||
private static final byte[] ENCRYPTED_FORMAT_MAGIC = "HVNENC".getBytes(StandardCharsets.UTF_8);
|
private static final Logger log = LoggerFactory.getLogger(KeyStorage.class);
|
||||||
private static final int ENCRYPTED_FORMAT_VERSION = 1;
|
|
||||||
private static final int ENCRYPTED_FORMAT_LENGTH = 4*2; // version,salt
|
|
||||||
|
|
||||||
public enum KeyEntry {
|
public enum KeyEntry {
|
||||||
MSG_SIGNATURE("sig", Sig.KEY_ALGO),
|
SYM_ENCRYPTION("sym.p12", Encryption.SYM_KEY_ALGO, "sym"), // symmetric encryption for persistence
|
||||||
MSG_ENCRYPTION("enc", Encryption.ASYM_KEY_ALGO);
|
MSG_SIGNATURE("sig.key", Sig.KEY_ALGO, "sig"),
|
||||||
|
MSG_ENCRYPTION("enc.key", Encryption.ASYM_KEY_ALGO, "enc");
|
||||||
|
|
||||||
private final String fileName;
|
private final String fileName;
|
||||||
private final String algorithm;
|
private final String algorithm;
|
||||||
|
private final String alias;
|
||||||
|
|
||||||
KeyEntry(String fileName, String algorithm) {
|
KeyEntry(String fileName, String algorithm, String alias) {
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
this.algorithm = algorithm;
|
this.algorithm = algorithm;
|
||||||
|
this.alias = alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFileName() {
|
public String getFileName() {
|
||||||
|
@ -96,6 +89,10 @@ public class KeyStorage {
|
||||||
return algorithm;
|
return algorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
@ -114,78 +111,40 @@ public class KeyStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allKeyFilesExist() {
|
public boolean allKeyFilesExist() {
|
||||||
return fileExists(KeyEntry.MSG_SIGNATURE) && fileExists(KeyEntry.MSG_ENCRYPTION);
|
return fileExists(KeyEntry.MSG_SIGNATURE) && fileExists(KeyEntry.MSG_ENCRYPTION) && fileExists(KeyEntry.SYM_ENCRYPTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean fileExists(KeyEntry keyEntry) {
|
private boolean fileExists(KeyEntry keyEntry) {
|
||||||
return new File(storageDir + "/" + keyEntry.getFileName() + ".key").exists();
|
return new File(storageDir + "/" + keyEntry.getFileName()).exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeyPair loadKeyPair(KeyEntry keyEntry, String password) throws IncorrectPasswordException {
|
private byte[] loadKeyBytes(KeyEntry keyEntry, SecretKey secretKey) {
|
||||||
FileUtil.rollingBackup(storageDir, keyEntry.getFileName() + ".key", 20);
|
File keyFile = new File(storageDir + "/" + keyEntry.getFileName());
|
||||||
// long now = System.currentTimeMillis();
|
try (FileInputStream fis = new FileInputStream(keyFile.getPath())) {
|
||||||
|
byte[] encodedKey = new byte[(int) keyFile.length()];
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
fis.read(encodedKey);
|
||||||
|
encodedKey = Encryption.decryptPayloadWithHmac(encodedKey, secretKey);
|
||||||
|
return encodedKey;
|
||||||
|
} catch (IOException | CryptoException e) {
|
||||||
|
log.error("Could not load key " + keyEntry.toString(), e.getMessage());
|
||||||
|
throw new RuntimeException("Could not load key " + keyEntry.toString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the public private KeyPair from a key file.
|
||||||
|
*
|
||||||
|
* @param keyEntry The key entry that defines the public private key
|
||||||
|
* @param secretKey The symmetric key that protects the key entry file
|
||||||
|
*/
|
||||||
|
public KeyPair loadKeyPair(KeyEntry keyEntry, SecretKey secretKey) {
|
||||||
try {
|
try {
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm());
|
KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm());
|
||||||
|
byte[] encodedPrivateKey = loadKeyBytes(keyEntry, secretKey);
|
||||||
|
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
|
||||||
|
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||||
PublicKey publicKey;
|
PublicKey publicKey;
|
||||||
PrivateKey privateKey;
|
|
||||||
|
|
||||||
File filePrivateKey = new File(storageDir + "/" + keyEntry.getFileName() + ".key");
|
|
||||||
try (FileInputStream fis = new FileInputStream(filePrivateKey.getPath())) {
|
|
||||||
byte[] encodedPrivateKey = new byte[(int) filePrivateKey.length()];
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
fis.read(encodedPrivateKey);
|
|
||||||
|
|
||||||
// Read magic bytes
|
|
||||||
byte[] magicBytes = Arrays.copyOfRange(encodedPrivateKey, 0, ENCRYPTED_FORMAT_MAGIC.length);
|
|
||||||
boolean isEncryptedPassword = Arrays.compare(magicBytes, ENCRYPTED_FORMAT_MAGIC) == 0;
|
|
||||||
if (isEncryptedPassword && password == null) {
|
|
||||||
throw new IncorrectPasswordException("Cannot load encrypted keys, user must open account with password " + filePrivateKey);
|
|
||||||
} else if (password != null && !isEncryptedPassword) {
|
|
||||||
log.warn("Password not needed for unencrypted key " + filePrivateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt using password
|
|
||||||
if (password != null) {
|
|
||||||
int position = ENCRYPTED_FORMAT_MAGIC.length;
|
|
||||||
|
|
||||||
// Read remaining header
|
|
||||||
ByteBuffer buf = ByteBuffer.wrap(encodedPrivateKey, position, ENCRYPTED_FORMAT_LENGTH);
|
|
||||||
position += ENCRYPTED_FORMAT_LENGTH;
|
|
||||||
int version = buf.getInt();
|
|
||||||
if (version != 1) throw new RuntimeException("Unable to parse encrypted keys");
|
|
||||||
int saltLength = buf.getInt();
|
|
||||||
|
|
||||||
// Read salt
|
|
||||||
byte[] salt = Arrays.copyOfRange(encodedPrivateKey, position, position + saltLength);
|
|
||||||
position += saltLength;
|
|
||||||
|
|
||||||
// Payload key derived from password
|
|
||||||
KeyCrypterScrypt crypter = ScryptUtil.getKeyCrypterScrypt(salt);
|
|
||||||
KeyParameter pwKey = ScryptUtil.deriveKeyWithScrypt(crypter, password);
|
|
||||||
SecretKey secretKey = new SecretKeySpec(pwKey.getKey(), Encryption.SYM_KEY_ALGO);
|
|
||||||
byte[] encryptedPayload = Arrays.copyOfRange(encodedPrivateKey, position, encodedPrivateKey.length);
|
|
||||||
|
|
||||||
// Decrypt key, handling exceptions caused by an incorrect password key
|
|
||||||
try {
|
|
||||||
encodedPrivateKey = Encryption.decryptPayloadWithHmac(encryptedPayload, secretKey);
|
|
||||||
} catch (CryptoException ce) {
|
|
||||||
// Most of the time (probably of slightly less than 255/256, around 99.61%) a bad password
|
|
||||||
// will result in BadPaddingException before HMAC check.
|
|
||||||
// See https://stackoverflow.com/questions/8049872/given-final-block-not-properly-padded
|
|
||||||
if (ce.getCause() instanceof BadPaddingException || Encryption.HMAC_ERROR_MSG.equals(ce.getMessage()))
|
|
||||||
throw new IncorrectPasswordException("Incorrect password");
|
|
||||||
else
|
|
||||||
throw ce;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
|
|
||||||
privateKey = keyFactory.generatePrivate(privateKeySpec);
|
|
||||||
} catch (InvalidKeySpecException | IOException | CryptoException e) {
|
|
||||||
log.error("Could not load key " + keyEntry.toString(), e.getMessage());
|
|
||||||
throw new RuntimeException("Could not load key " + keyEntry.toString(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (privateKey instanceof RSAPrivateCrtKey) {
|
if (privateKey instanceof RSAPrivateCrtKey) {
|
||||||
RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey;
|
RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey;
|
||||||
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent());
|
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent());
|
||||||
|
@ -202,8 +161,6 @@ public class KeyStorage {
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("Unsupported key algo" + keyEntry.getAlgorithm());
|
throw new RuntimeException("Unsupported key algo" + keyEntry.getAlgorithm());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("load completed in {} msec", System.currentTimeMillis() - new Date().getTime());
|
|
||||||
return new KeyPair(publicKey, privateKey);
|
return new KeyPair(publicKey, privateKey);
|
||||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||||
log.error("Could not load key " + keyEntry.toString(), e);
|
log.error("Could not load key " + keyEntry.toString(), e);
|
||||||
|
@ -211,46 +168,108 @@ public class KeyStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveKeyRing(KeyRing keyRing, String password) {
|
/**
|
||||||
savePrivateKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName(), password);
|
* Loads the password protected symmetric secret key for this key ring.
|
||||||
savePrivateKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName(), password);
|
*
|
||||||
|
* @param keyEntry The key entry that defines the symmetric key
|
||||||
|
* @param password Optional password that protects the key
|
||||||
|
*/
|
||||||
|
public SecretKey loadSecretKey(KeyEntry keyEntry, String password) throws IncorrectPasswordException {
|
||||||
|
char[] passwordChars = password == null ? new char[0] : password.toCharArray();
|
||||||
|
try {
|
||||||
|
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||||
|
keyStore.load(new FileInputStream(storageDir + "/" + keyEntry.getFileName()), passwordChars);
|
||||||
|
Key key = keyStore.getKey(keyEntry.getAlias(), passwordChars);
|
||||||
|
return (SecretKey) key;
|
||||||
|
} catch (UnrecoverableKeyException e) { // null password when password is required
|
||||||
|
throw new IncorrectPasswordException("Incorrect password");
|
||||||
|
} catch (IOException e) { // incorrect password
|
||||||
|
if (e.getCause() instanceof UnrecoverableKeyException) {
|
||||||
|
throw new IncorrectPasswordException("Incorrect password");
|
||||||
|
} else {
|
||||||
|
log.error("Could not load key " + keyEntry.toString(), e);
|
||||||
|
throw new RuntimeException("Could not load key " + keyEntry.toString(), e);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Could not load key " + keyEntry.toString(), e);
|
||||||
|
throw new RuntimeException("Could not load key " + keyEntry.toString(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void savePrivateKey(PrivateKey privateKey, String name, String password) {
|
/**
|
||||||
|
* Saves the key ring to the key storage directory.
|
||||||
|
*
|
||||||
|
* @param keyRing The key ring
|
||||||
|
* @param password Optional password
|
||||||
|
*/
|
||||||
|
public void saveKeyRing(KeyRing keyRing, String oldPassword, String password) {
|
||||||
|
SecretKey symmetric = keyRing.getSymmetricKey();
|
||||||
|
|
||||||
|
// password protect the symmetric key
|
||||||
|
saveKey(symmetric, KeyEntry.SYM_ENCRYPTION.getAlias(), KeyEntry.SYM_ENCRYPTION.getFileName(), oldPassword, password);
|
||||||
|
|
||||||
|
// use symmetric encryption to encrypt the key pairs
|
||||||
|
saveKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName(), symmetric);
|
||||||
|
saveKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName(), symmetric);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves private key in PKCS#8 to a file and encrypts using the symmetric key.
|
||||||
|
*
|
||||||
|
* @param key The key pair
|
||||||
|
* @param fileName File name to save
|
||||||
|
* @param secretKey Secret key to encrypt the key pair
|
||||||
|
*/
|
||||||
|
private void saveKey(PrivateKey key, String fileName, SecretKey secretKey) {
|
||||||
if (!storageDir.exists())
|
if (!storageDir.exists())
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
storageDir.mkdirs();
|
storageDir.mkdirs();
|
||||||
|
|
||||||
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
|
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(key.getEncoded());
|
||||||
try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + name + ".key")) {
|
byte[] keyBytes = pkcs8EncodedKeySpec.getEncoded();
|
||||||
byte[] keyBytes = pkcs8EncodedKeySpec.getEncoded();
|
try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + fileName)) {
|
||||||
// Encrypt
|
keyBytes = Encryption.encryptPayloadWithHmac(keyBytes, secretKey);
|
||||||
if (password != null) {
|
|
||||||
// Magic
|
|
||||||
fos.write(ENCRYPTED_FORMAT_MAGIC);
|
|
||||||
|
|
||||||
// Version, salt length
|
|
||||||
ByteBuffer header = ByteBuffer.allocate(ENCRYPTED_FORMAT_LENGTH);
|
|
||||||
header.putInt(ENCRYPTED_FORMAT_VERSION);
|
|
||||||
header.putInt(SALT_LENGTH);
|
|
||||||
fos.write(header.array());
|
|
||||||
|
|
||||||
// Salt value
|
|
||||||
byte[] salt = CryptoUtils.getRandomBytes(SALT_LENGTH);
|
|
||||||
fos.write(salt);
|
|
||||||
|
|
||||||
// Generate secret from password key and salt
|
|
||||||
KeyCrypterScrypt crypter = ScryptUtil.getKeyCrypterScrypt(salt);
|
|
||||||
KeyParameter pwKey = ScryptUtil.deriveKeyWithScrypt(crypter, password);
|
|
||||||
SecretKey secretKey = new SecretKeySpec(pwKey.getKey(), Encryption.SYM_KEY_ALGO);
|
|
||||||
|
|
||||||
// Encrypt payload
|
|
||||||
keyBytes = Encryption.encryptPayloadWithHmac(keyBytes, secretKey);
|
|
||||||
}
|
|
||||||
fos.write(keyBytes);
|
fos.write(keyBytes);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Could not save key " + name, e);
|
log.error("Could not save key " + fileName, e);
|
||||||
throw new RuntimeException("Could not save key " + name, e);
|
throw new RuntimeException("Could not save key " + fileName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a SecretKey to a PKCS12 file.
|
||||||
|
*
|
||||||
|
* @param key The symmetric key
|
||||||
|
* @param alias Alias of the key entry in the key store
|
||||||
|
* @param fileName Filename of the key store
|
||||||
|
* @param oldPassword Optional password to decrypt existing key store
|
||||||
|
* @param password Optional password to encrypt the key store
|
||||||
|
*/
|
||||||
|
private void saveKey(SecretKey key, String alias, String fileName, String oldPassword, String password) {
|
||||||
|
if (!storageDir.exists())
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
storageDir.mkdirs();
|
||||||
|
|
||||||
|
var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray();
|
||||||
|
var passwordChars = password == null ? new char[0] : password.toCharArray();
|
||||||
|
try {
|
||||||
|
var path = storageDir + "/" + fileName;
|
||||||
|
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||||
|
|
||||||
|
// load from existing file or initialize new
|
||||||
|
try {
|
||||||
|
keyStore.load(new FileInputStream(path), oldPasswordChars);
|
||||||
|
} catch (Exception e) {
|
||||||
|
keyStore.load(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// store in the keystore
|
||||||
|
keyStore.setKeyEntry(alias, key, passwordChars, null);
|
||||||
|
|
||||||
|
// save the keystore
|
||||||
|
keyStore.store(new FileOutputStream(path), passwordChars);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Could not save key " + alias, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ import bisq.common.Timer;
|
||||||
import bisq.common.UserThread;
|
import bisq.common.UserThread;
|
||||||
import bisq.common.app.DevEnv;
|
import bisq.common.app.DevEnv;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.crypto.CryptoException;
|
||||||
|
import bisq.common.crypto.Encryption;
|
||||||
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.file.CorruptedStorageFileHandler;
|
import bisq.common.file.CorruptedStorageFileHandler;
|
||||||
import bisq.common.file.FileUtil;
|
import bisq.common.file.FileUtil;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
@ -34,6 +37,7 @@ import javax.inject.Named;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
@ -208,6 +212,8 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||||
private final File dir;
|
private final File dir;
|
||||||
private final PersistenceProtoResolver persistenceProtoResolver;
|
private final PersistenceProtoResolver persistenceProtoResolver;
|
||||||
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
||||||
|
@Nullable
|
||||||
|
private final KeyRing keyRing;
|
||||||
private File storageFile;
|
private File storageFile;
|
||||||
private T persistable;
|
private T persistable;
|
||||||
private String fileName;
|
private String fileName;
|
||||||
|
@ -228,10 +234,12 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||||
@Inject
|
@Inject
|
||||||
public PersistenceManager(@Named(Config.STORAGE_DIR) File dir,
|
public PersistenceManager(@Named(Config.STORAGE_DIR) File dir,
|
||||||
PersistenceProtoResolver persistenceProtoResolver,
|
PersistenceProtoResolver persistenceProtoResolver,
|
||||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||||
|
@Nullable KeyRing keyRing) {
|
||||||
this.dir = checkDir(dir);
|
this.dir = checkDir(dir);
|
||||||
this.persistenceProtoResolver = persistenceProtoResolver;
|
this.persistenceProtoResolver = persistenceProtoResolver;
|
||||||
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
||||||
|
this.keyRing = keyRing;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -337,6 +345,10 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||||
log.warn("We have started the shut down routine already. We ignore that getPersisted call.");
|
log.warn("We have started the shut down routine already. We ignore that getPersisted call.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (keyRing != null && !keyRing.isUnlocked()) {
|
||||||
|
log.warn("Account is not open yet, ignoring getPersisted.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
readCalled.set(true);
|
readCalled.set(true);
|
||||||
|
|
||||||
|
@ -347,7 +359,21 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||||
|
|
||||||
long ts = System.currentTimeMillis();
|
long ts = System.currentTimeMillis();
|
||||||
try (FileInputStream fileInputStream = new FileInputStream(storageFile)) {
|
try (FileInputStream fileInputStream = new FileInputStream(storageFile)) {
|
||||||
protobuf.PersistableEnvelope proto = protobuf.PersistableEnvelope.parseDelimitedFrom(fileInputStream);
|
protobuf.PersistableEnvelope proto;
|
||||||
|
if (keyRing != null) {
|
||||||
|
byte[] encryptedBytes = fileInputStream.readAllBytes();
|
||||||
|
try {
|
||||||
|
byte[] decryptedBytes = Encryption.decryptPayloadWithHmac(encryptedBytes, keyRing.getSymmetricKey());
|
||||||
|
proto = protobuf.PersistableEnvelope.parseFrom(decryptedBytes);
|
||||||
|
} catch (CryptoException ce) {
|
||||||
|
log.warn("Expected encrypted persisted file, attempting to getPersisted without decryption");
|
||||||
|
ByteArrayInputStream bs = new ByteArrayInputStream(encryptedBytes);
|
||||||
|
proto = protobuf.PersistableEnvelope.parseDelimitedFrom(bs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proto = protobuf.PersistableEnvelope.parseDelimitedFrom(fileInputStream);
|
||||||
|
}
|
||||||
|
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
T persistableEnvelope = (T) persistenceProtoResolver.fromProto(proto);
|
T persistableEnvelope = (T) persistenceProtoResolver.fromProto(proto);
|
||||||
log.info("Reading {} completed in {} ms", fileName, System.currentTimeMillis() - ts);
|
log.info("Reading {} completed in {} ms", fileName, System.currentTimeMillis() - ts);
|
||||||
|
@ -434,7 +460,16 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||||
public void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler) {
|
public void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler) {
|
||||||
if (!allServicesInitialized.get()) {
|
if (!allServicesInitialized.get()) {
|
||||||
log.warn("Application has not completed start up yet so we do not permit writing data to disk.");
|
log.warn("Application has not completed start up yet so we do not permit writing data to disk.");
|
||||||
UserThread.execute(completeHandler);
|
if (completeHandler != null) {
|
||||||
|
UserThread.execute(completeHandler);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keyRing != null && !keyRing.isUnlocked()) {
|
||||||
|
log.warn("Account is not open, ignoring writeToDisk.");
|
||||||
|
if (completeHandler != null) {
|
||||||
|
UserThread.execute(completeHandler);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,7 +492,12 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||||
|
|
||||||
fileOutputStream = new FileOutputStream(tempFile);
|
fileOutputStream = new FileOutputStream(tempFile);
|
||||||
|
|
||||||
serialized.writeDelimitedTo(fileOutputStream);
|
if (keyRing != null) {
|
||||||
|
byte[] encryptedBytes = Encryption.encryptPayloadWithHmac(serialized.toByteArray(), keyRing.getSymmetricKey());
|
||||||
|
fileOutputStream.write(encryptedBytes);
|
||||||
|
} else {
|
||||||
|
serialized.writeDelimitedTo(fileOutputStream);
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide
|
// Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide
|
||||||
// to not write through to physical media for at least a few seconds, but this is the best we can do.
|
// to not write through to physical media for at least a few seconds, but this is the best we can do.
|
||||||
|
|
|
@ -35,7 +35,7 @@ public class ZipUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zips directory into the output stream. Empty directories are not included.
|
* Zips directory into the output stream. Empty directories are not included.
|
||||||
*
|
*
|
||||||
* @param dir The directory to create the zip from.
|
* @param dir The directory to create the zip from.
|
||||||
* @param out The stream to write to.
|
* @param out The stream to write to.
|
||||||
*/
|
*/
|
||||||
|
@ -64,6 +64,8 @@ public class ZipUtils {
|
||||||
|
|
||||||
// Close the zip entry.
|
// Close the zip entry.
|
||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,7 +90,7 @@ public class ZipUtils {
|
||||||
/**
|
/**
|
||||||
* Unzips the zipStream into the specified directory, overwriting any files.
|
* Unzips the zipStream into the specified directory, overwriting any files.
|
||||||
* Existing files are preserved.
|
* Existing files are preserved.
|
||||||
*
|
*
|
||||||
* @param dir The directory to write to.
|
* @param dir The directory to write to.
|
||||||
* @param inputStream The raw stream assumed to be in zip format.
|
* @param inputStream The raw stream assumed to be in zip format.
|
||||||
* @param bufferSize The buffer used to read from efficiently.
|
* @param bufferSize The buffer used to read from efficiently.
|
||||||
|
|
|
@ -50,15 +50,15 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CoreAccountService {
|
public class CoreAccountService {
|
||||||
|
|
||||||
private final Config config;
|
private final Config config;
|
||||||
private final KeyStorage keyStorage;
|
private final KeyStorage keyStorage;
|
||||||
private final KeyRing keyRing;
|
private final KeyRing keyRing;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private String password;
|
private String password;
|
||||||
private List<AccountServiceListener> listeners = new ArrayList<AccountServiceListener>();
|
private List<AccountServiceListener> listeners = new ArrayList<AccountServiceListener>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CoreAccountService(Config config,
|
public CoreAccountService(Config config,
|
||||||
KeyStorage keyStorage,
|
KeyStorage keyStorage,
|
||||||
|
@ -67,34 +67,34 @@ public class CoreAccountService {
|
||||||
this.keyStorage = keyStorage;
|
this.keyStorage = keyStorage;
|
||||||
this.keyRing = keyRing;
|
this.keyRing = keyRing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(AccountServiceListener listener) {
|
public void addListener(AccountServiceListener listener) {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean removeListener(AccountServiceListener listener) {
|
public boolean removeListener(AccountServiceListener listener) {
|
||||||
return listeners.remove(listener);
|
return listeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean accountExists() {
|
public boolean accountExists() {
|
||||||
return keyStorage.allKeyFilesExist(); // public and private key pair indicate the existence of the account
|
return keyStorage.allKeyFilesExist(); // public and private key pair indicate the existence of the account
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAccountOpen() {
|
public boolean isAccountOpen() {
|
||||||
return keyRing.isUnlocked() && accountExists();
|
return keyRing.isUnlocked() && accountExists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkAccountOpen() {
|
public void checkAccountOpen() {
|
||||||
checkState(isAccountOpen(), "Account not open");
|
checkState(isAccountOpen(), "Account not open");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createAccount(String password) {
|
public void createAccount(String password) {
|
||||||
if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists");
|
if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists");
|
||||||
keyRing.generateKeys(password);
|
keyRing.generateKeys(password);
|
||||||
this.password = password;
|
this.password = password;
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountCreated();
|
for (AccountServiceListener listener : listeners) listener.onAccountCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openAccount(String password) throws IncorrectPasswordException {
|
public void openAccount(String password) throws IncorrectPasswordException {
|
||||||
if (!accountExists()) throw new IllegalStateException("Cannot open account if account does not exist");
|
if (!accountExists()) throw new IllegalStateException("Cannot open account if account does not exist");
|
||||||
if (keyRing.unlockKeys(password, false)) {
|
if (keyRing.unlockKeys(password, false)) {
|
||||||
|
@ -104,21 +104,21 @@ public class CoreAccountService {
|
||||||
throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen");
|
throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changePassword(String password) {
|
public void changePassword(String password) {
|
||||||
if (!isAccountOpen()) throw new IllegalStateException("Cannot change password on unopened account");
|
if (!isAccountOpen()) throw new IllegalStateException("Cannot change password on unopened account");
|
||||||
keyStorage.saveKeyRing(keyRing, password);
|
|
||||||
String oldPassword = this.password;
|
String oldPassword = this.password;
|
||||||
|
keyStorage.saveKeyRing(keyRing, oldPassword, password);
|
||||||
this.password = password;
|
this.password = password;
|
||||||
for (AccountServiceListener listener : listeners) listener.onPasswordChanged(oldPassword, password);
|
for (AccountServiceListener listener : listeners) listener.onPasswordChanged(oldPassword, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeAccount() {
|
public void closeAccount() {
|
||||||
if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account");
|
if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account");
|
||||||
keyRing.lockKeys(); // closed account means the keys are locked
|
keyRing.lockKeys(); // closed account means the keys are locked
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountClosed();
|
for (AccountServiceListener listener : listeners) listener.onAccountClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) {
|
public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) {
|
||||||
if (!accountExists()) throw new IllegalStateException("Cannot backup non existing account");
|
if (!accountExists()) throw new IllegalStateException("Cannot backup non existing account");
|
||||||
|
|
||||||
|
@ -142,14 +142,14 @@ public class CoreAccountService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void restoreAccount(InputStream inputStream, int bufferSize, Runnable onShutdown) throws Exception {
|
public void restoreAccount(InputStream inputStream, int bufferSize, Runnable onShutdown) throws Exception {
|
||||||
if (accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
|
if (accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
|
||||||
File dataDir = new File(config.appDataDir.getPath());
|
File dataDir = new File(config.appDataDir.getPath());
|
||||||
ZipUtils.unzipToDir(dataDir, inputStream, bufferSize);
|
ZipUtils.unzipToDir(dataDir, inputStream, bufferSize);
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown);
|
for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteAccount(Runnable onShutdown) {
|
public void deleteAccount(Runnable onShutdown) {
|
||||||
try {
|
try {
|
||||||
keyRing.lockKeys();
|
keyRing.lockKeys();
|
||||||
|
|
|
@ -6,6 +6,8 @@ import bisq.core.trade.TradableList;
|
||||||
import bisq.network.p2p.P2PService;
|
import bisq.network.p2p.P2PService;
|
||||||
import bisq.network.p2p.peers.PeerManager;
|
import bisq.network.p2p.peers.PeerManager;
|
||||||
|
|
||||||
|
import bisq.common.crypto.KeyRing;
|
||||||
|
import bisq.common.crypto.KeyStorage;
|
||||||
import bisq.common.file.CorruptedStorageFileHandler;
|
import bisq.common.file.CorruptedStorageFileHandler;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
@ -34,8 +36,9 @@ public class OpenOfferManagerTest {
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
var corruptedStorageFileHandler = mock(CorruptedStorageFileHandler.class);
|
var corruptedStorageFileHandler = mock(CorruptedStorageFileHandler.class);
|
||||||
var storageDir = Files.createTempDirectory("storage").toFile();
|
var storageDir = Files.createTempDirectory("storage").toFile();
|
||||||
persistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler);
|
var keyRing = new KeyRing(new KeyStorage(storageDir));
|
||||||
signedOfferPersistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler);
|
persistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler, keyRing);
|
||||||
|
signedOfferPersistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler, keyRing);
|
||||||
coreContext = new CoreContext();
|
coreContext = new CoreContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import bisq.core.trade.TradeManager;
|
||||||
import bisq.core.user.Preferences;
|
import bisq.core.user.Preferences;
|
||||||
import bisq.core.user.User;
|
import bisq.core.user.User;
|
||||||
|
|
||||||
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.file.CorruptedStorageFileHandler;
|
import bisq.common.file.CorruptedStorageFileHandler;
|
||||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||||
|
|
||||||
|
@ -59,6 +60,7 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||||
private final String accountsFileName = "AltcoinPaymentAccounts";
|
private final String accountsFileName = "AltcoinPaymentAccounts";
|
||||||
private final PersistenceProtoResolver persistenceProtoResolver;
|
private final PersistenceProtoResolver persistenceProtoResolver;
|
||||||
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
||||||
|
private final KeyRing keyRing;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public AltCoinAccountsDataModel(User user,
|
public AltCoinAccountsDataModel(User user,
|
||||||
|
@ -67,7 +69,8 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
AccountAgeWitnessService accountAgeWitnessService,
|
AccountAgeWitnessService accountAgeWitnessService,
|
||||||
PersistenceProtoResolver persistenceProtoResolver,
|
PersistenceProtoResolver persistenceProtoResolver,
|
||||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||||
|
KeyRing keyRing) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
this.openOfferManager = openOfferManager;
|
this.openOfferManager = openOfferManager;
|
||||||
|
@ -75,6 +78,7 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||||
this.persistenceProtoResolver = persistenceProtoResolver;
|
this.persistenceProtoResolver = persistenceProtoResolver;
|
||||||
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
||||||
|
this.keyRing = keyRing;
|
||||||
setChangeListener = change -> fillAndSortPaymentAccounts();
|
setChangeListener = change -> fillAndSortPaymentAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,12 +154,12 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||||
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
|
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
|
||||||
.filter(paymentAccount -> paymentAccount instanceof AssetAccount)
|
.filter(paymentAccount -> paymentAccount instanceof AssetAccount)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
|
GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void importAccounts(Stage stage) {
|
public void importAccounts(Stage stage) {
|
||||||
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
|
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getNumPaymentAccounts() {
|
public int getNumPaymentAccounts() {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import bisq.core.trade.TradeManager;
|
||||||
import bisq.core.user.Preferences;
|
import bisq.core.user.Preferences;
|
||||||
import bisq.core.user.User;
|
import bisq.core.user.User;
|
||||||
|
|
||||||
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.file.CorruptedStorageFileHandler;
|
import bisq.common.file.CorruptedStorageFileHandler;
|
||||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||||
private final String accountsFileName = "FiatPaymentAccounts";
|
private final String accountsFileName = "FiatPaymentAccounts";
|
||||||
private final PersistenceProtoResolver persistenceProtoResolver;
|
private final PersistenceProtoResolver persistenceProtoResolver;
|
||||||
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
||||||
|
private final KeyRing keyRing;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FiatAccountsDataModel(User user,
|
public FiatAccountsDataModel(User user,
|
||||||
|
@ -68,7 +70,8 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
AccountAgeWitnessService accountAgeWitnessService,
|
AccountAgeWitnessService accountAgeWitnessService,
|
||||||
PersistenceProtoResolver persistenceProtoResolver,
|
PersistenceProtoResolver persistenceProtoResolver,
|
||||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||||
|
KeyRing keyRing) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
this.openOfferManager = openOfferManager;
|
this.openOfferManager = openOfferManager;
|
||||||
|
@ -76,6 +79,7 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||||
this.persistenceProtoResolver = persistenceProtoResolver;
|
this.persistenceProtoResolver = persistenceProtoResolver;
|
||||||
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
||||||
|
this.keyRing = keyRing;
|
||||||
setChangeListener = change -> fillAndSortPaymentAccounts();
|
setChangeListener = change -> fillAndSortPaymentAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,12 +157,12 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||||
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
|
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
|
||||||
.filter(paymentAccount -> !(paymentAccount instanceof AssetAccount))
|
.filter(paymentAccount -> !(paymentAccount instanceof AssetAccount))
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
|
GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void importAccounts(Stage stage) {
|
public void importAccounts(Stage stage) {
|
||||||
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
|
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getNumPaymentAccounts() {
|
public int getNumPaymentAccounts() {
|
||||||
|
|
|
@ -54,6 +54,7 @@ import bisq.network.p2p.P2PService;
|
||||||
import bisq.common.UserThread;
|
import bisq.common.UserThread;
|
||||||
import bisq.common.app.DevEnv;
|
import bisq.common.app.DevEnv;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.file.CorruptedStorageFileHandler;
|
import bisq.common.file.CorruptedStorageFileHandler;
|
||||||
import bisq.common.persistence.PersistenceManager;
|
import bisq.common.persistence.PersistenceManager;
|
||||||
import bisq.common.proto.persistable.PersistableEnvelope;
|
import bisq.common.proto.persistable.PersistableEnvelope;
|
||||||
|
@ -203,11 +204,12 @@ public class GUIUtil {
|
||||||
Preferences preferences,
|
Preferences preferences,
|
||||||
Stage stage,
|
Stage stage,
|
||||||
PersistenceProtoResolver persistenceProtoResolver,
|
PersistenceProtoResolver persistenceProtoResolver,
|
||||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||||
|
KeyRing keyRing) {
|
||||||
if (!accounts.isEmpty()) {
|
if (!accounts.isEmpty()) {
|
||||||
String directory = getDirectoryFromChooser(preferences, stage);
|
String directory = getDirectoryFromChooser(preferences, stage);
|
||||||
if (!directory.isEmpty()) {
|
if (!directory.isEmpty()) {
|
||||||
PersistenceManager<PersistableEnvelope> persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler);
|
PersistenceManager<PersistableEnvelope> persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||||
PaymentAccountList paymentAccounts = new PaymentAccountList(accounts);
|
PaymentAccountList paymentAccounts = new PaymentAccountList(accounts);
|
||||||
persistenceManager.initialize(paymentAccounts, fileName, PersistenceManager.Source.PRIVATE_LOW_PRIO);
|
persistenceManager.initialize(paymentAccounts, fileName, PersistenceManager.Source.PRIVATE_LOW_PRIO);
|
||||||
persistenceManager.persistNow(() -> {
|
persistenceManager.persistNow(() -> {
|
||||||
|
@ -227,7 +229,8 @@ public class GUIUtil {
|
||||||
Preferences preferences,
|
Preferences preferences,
|
||||||
Stage stage,
|
Stage stage,
|
||||||
PersistenceProtoResolver persistenceProtoResolver,
|
PersistenceProtoResolver persistenceProtoResolver,
|
||||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||||
|
KeyRing keyRing) {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
File initDir = new File(preferences.getDirectoryChooserPath());
|
File initDir = new File(preferences.getDirectoryChooserPath());
|
||||||
if (initDir.isDirectory()) {
|
if (initDir.isDirectory()) {
|
||||||
|
@ -240,7 +243,7 @@ public class GUIUtil {
|
||||||
if (Paths.get(path).getFileName().toString().equals(fileName)) {
|
if (Paths.get(path).getFileName().toString().equals(fileName)) {
|
||||||
String directory = Paths.get(path).getParent().toString();
|
String directory = Paths.get(path).getParent().toString();
|
||||||
preferences.setDirectoryChooserPath(directory);
|
preferences.setDirectoryChooserPath(directory);
|
||||||
PersistenceManager<PaymentAccountList> persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler);
|
PersistenceManager<PaymentAccountList> persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||||
persistenceManager.readPersisted(fileName, persisted -> {
|
persistenceManager.readPersisted(fileName, persisted -> {
|
||||||
StringBuilder msg = new StringBuilder();
|
StringBuilder msg = new StringBuilder();
|
||||||
HashSet<PaymentAccount> paymentAccounts = new HashSet<>();
|
HashSet<PaymentAccount> paymentAccounts = new HashSet<>();
|
||||||
|
|
|
@ -131,7 +131,7 @@ public class P2PNetworkLoad extends Metric implements MessageListener, SetupList
|
||||||
CorePersistenceProtoResolver persistenceProtoResolver = new CorePersistenceProtoResolver(null, null, networkProtoResolver);
|
CorePersistenceProtoResolver persistenceProtoResolver = new CorePersistenceProtoResolver(null, null, networkProtoResolver);
|
||||||
DefaultSeedNodeRepository seedNodeRepository = new DefaultSeedNodeRepository(config);
|
DefaultSeedNodeRepository seedNodeRepository = new DefaultSeedNodeRepository(config);
|
||||||
PeerManager peerManager = new PeerManager(networkNode, seedNodeRepository, new ClockWatcher(),
|
PeerManager peerManager = new PeerManager(networkNode, seedNodeRepository, new ClockWatcher(),
|
||||||
new PersistenceManager<>(torHiddenServiceDir, persistenceProtoResolver, corruptedStorageFileHandler), maxConnections);
|
new PersistenceManager<>(torHiddenServiceDir, persistenceProtoResolver, corruptedStorageFileHandler, null), maxConnections);
|
||||||
|
|
||||||
// init file storage
|
// init file storage
|
||||||
peerManager.readPersisted(() -> {
|
peerManager.readPersisted(() -> {
|
||||||
|
|
|
@ -120,7 +120,7 @@ public abstract class P2PSeedNodeSnapshotBase extends Metric implements MessageL
|
||||||
//TODO will not work with historical data... should be refactored to re-use code for reading resource files
|
//TODO will not work with historical data... should be refactored to re-use code for reading resource files
|
||||||
TradeStatistics3Store tradeStatistics3Store = new TradeStatistics3Store();
|
TradeStatistics3Store tradeStatistics3Store = new TradeStatistics3Store();
|
||||||
PersistenceManager<TradeStatistics3Store> tradeStatistics3PersistenceManager = new PersistenceManager<>(dir,
|
PersistenceManager<TradeStatistics3Store> tradeStatistics3PersistenceManager = new PersistenceManager<>(dir,
|
||||||
persistenceProtoResolver, null);
|
persistenceProtoResolver, null, null);
|
||||||
tradeStatistics3PersistenceManager.initialize(tradeStatistics3Store,
|
tradeStatistics3PersistenceManager.initialize(tradeStatistics3Store,
|
||||||
tradeStatistics3Store.getDefaultStorageFileName() + networkPostfix,
|
tradeStatistics3Store.getDefaultStorageFileName() + networkPostfix,
|
||||||
PersistenceManager.Source.NETWORK);
|
PersistenceManager.Source.NETWORK);
|
||||||
|
@ -133,7 +133,7 @@ public abstract class P2PSeedNodeSnapshotBase extends Metric implements MessageL
|
||||||
|
|
||||||
AccountAgeWitnessStore accountAgeWitnessStore = new AccountAgeWitnessStore();
|
AccountAgeWitnessStore accountAgeWitnessStore = new AccountAgeWitnessStore();
|
||||||
PersistenceManager<AccountAgeWitnessStore> accountAgeWitnessPersistenceManager = new PersistenceManager<>(dir,
|
PersistenceManager<AccountAgeWitnessStore> accountAgeWitnessPersistenceManager = new PersistenceManager<>(dir,
|
||||||
persistenceProtoResolver, null);
|
persistenceProtoResolver, null, null);
|
||||||
accountAgeWitnessPersistenceManager.initialize(accountAgeWitnessStore,
|
accountAgeWitnessPersistenceManager.initialize(accountAgeWitnessStore,
|
||||||
accountAgeWitnessStore.getDefaultStorageFileName() + networkPostfix,
|
accountAgeWitnessStore.getDefaultStorageFileName() + networkPostfix,
|
||||||
PersistenceManager.Source.NETWORK);
|
PersistenceManager.Source.NETWORK);
|
||||||
|
|
|
@ -30,6 +30,8 @@ import bisq.network.p2p.peers.peerexchange.PeerList;
|
||||||
import bisq.network.p2p.seed.SeedNodeRepository;
|
import bisq.network.p2p.seed.SeedNodeRepository;
|
||||||
|
|
||||||
import bisq.common.ClockWatcher;
|
import bisq.common.ClockWatcher;
|
||||||
|
import bisq.common.crypto.KeyRing;
|
||||||
|
import bisq.common.crypto.KeyStorage;
|
||||||
import bisq.common.file.CorruptedStorageFileHandler;
|
import bisq.common.file.CorruptedStorageFileHandler;
|
||||||
import bisq.common.persistence.PersistenceManager;
|
import bisq.common.persistence.PersistenceManager;
|
||||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||||
|
@ -64,7 +66,7 @@ public class MockNode {
|
||||||
this.maxConnections = maxConnections;
|
this.maxConnections = maxConnections;
|
||||||
networkNode = mock(NetworkNode.class);
|
networkNode = mock(NetworkNode.class);
|
||||||
File storageDir = Files.createTempDirectory("storage").toFile();
|
File storageDir = Files.createTempDirectory("storage").toFile();
|
||||||
persistenceManager = new PersistenceManager<>(storageDir, mock(PersistenceProtoResolver.class), mock(CorruptedStorageFileHandler.class));
|
persistenceManager = new PersistenceManager<>(storageDir, mock(PersistenceProtoResolver.class), mock(CorruptedStorageFileHandler.class), mock(KeyRing.class));
|
||||||
peerManager = new PeerManager(networkNode, mock(SeedNodeRepository.class), new ClockWatcher(), persistenceManager, maxConnections);
|
peerManager = new PeerManager(networkNode, mock(SeedNodeRepository.class), new ClockWatcher(), persistenceManager, maxConnections);
|
||||||
connections = new HashSet<>();
|
connections = new HashSet<>();
|
||||||
when(networkNode.getAllConnections()).thenReturn(connections);
|
when(networkNode.getAllConnections()).thenReturn(connections);
|
||||||
|
|
Loading…
Reference in a new issue