mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-10-01 01:35:48 -04:00
Encrypt persisted data using password protected symmetric key (#279)
This commit is contained in:
parent
75c66ee43f
commit
5b38eab716
@ -218,9 +218,9 @@ public class Encryption {
|
||||
|
||||
public static SecretKey generateSecretKey(int bits) {
|
||||
try {
|
||||
KeyGenerator keyPairGenerator = KeyGenerator.getInstance(SYM_KEY_ALGO);
|
||||
keyPairGenerator.init(bits);
|
||||
return keyPairGenerator.generateKey();
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(SYM_KEY_ALGO);
|
||||
keyGenerator.init(bits);
|
||||
return keyGenerator.generateKey();
|
||||
} catch (Throwable e) {
|
||||
log.error("Couldn't generate key", e);
|
||||
throw new RuntimeException("Couldn't generate key");
|
||||
|
@ -20,6 +20,8 @@ package bisq.common.crypto;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import java.security.KeyPair;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
@ -36,6 +38,7 @@ public final class KeyRing {
|
||||
|
||||
private final KeyStorage keyStorage;
|
||||
|
||||
private SecretKey symmetricKey;
|
||||
private KeyPair signatureKeyPair;
|
||||
private KeyPair encryptionKeyPair;
|
||||
private PubKeyRing pubKeyRing;
|
||||
@ -67,7 +70,8 @@ public final class KeyRing {
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
boolean isUnlocked = this.signatureKeyPair != null
|
||||
boolean isUnlocked = this.symmetricKey != null
|
||||
&& this.signatureKeyPair != null
|
||||
&& this.encryptionKeyPair != null
|
||||
&& this.pubKeyRing != null;
|
||||
return isUnlocked;
|
||||
@ -80,6 +84,7 @@ public final class KeyRing {
|
||||
public void lockKeys() {
|
||||
signatureKeyPair = null;
|
||||
encryptionKeyPair = null;
|
||||
symmetricKey = null;
|
||||
pubKeyRing = null;
|
||||
}
|
||||
|
||||
@ -93,8 +98,9 @@ public final class KeyRing {
|
||||
public boolean unlockKeys(@Nullable String password, boolean generateKeys) throws IncorrectPasswordException {
|
||||
if (isUnlocked()) return true;
|
||||
if (keyStorage.allKeyFilesExist()) {
|
||||
signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE, password);
|
||||
encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION, password);
|
||||
symmetricKey = keyStorage.loadSecretKey(KeyStorage.KeyEntry.SYM_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());
|
||||
} else if (generateKeys) {
|
||||
generateKeys(password);
|
||||
@ -109,17 +115,19 @@ public final class KeyRing {
|
||||
*/
|
||||
public void generateKeys(String password) {
|
||||
if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
|
||||
symmetricKey = Encryption.generateSecretKey(256);
|
||||
signatureKeyPair = Sig.generateKeyPair();
|
||||
encryptionKeyPair = Encryption.generateKeyPair();
|
||||
pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
||||
keyStorage.saveKeyRing(this, password);
|
||||
keyStorage.saveKeyRing(this, null, password);
|
||||
}
|
||||
|
||||
// Don't print keys for security reasons
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KeyRing{" +
|
||||
"signatureKeyPair.hashCode()=" + signatureKeyPair.hashCode() +
|
||||
"symmetricKey.hashCode()=" + symmetricKey.hashCode() +
|
||||
", signatureKeyPair.hashCode()=" + signatureKeyPair.hashCode() +
|
||||
", encryptionKeyPair.hashCode()=" + encryptionKeyPair.hashCode() +
|
||||
", pubKeyRing.hashCode()=" + pubKeyRing.hashCode() +
|
||||
'}';
|
||||
|
@ -18,26 +18,22 @@
|
||||
package bisq.common.crypto;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.file.FileUtil;
|
||||
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyStore;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.interfaces.DSAParams;
|
||||
import java.security.interfaces.DSAPrivateKey;
|
||||
import java.security.interfaces.RSAPrivateCrtKey;
|
||||
@ -47,9 +43,6 @@ import java.security.spec.KeySpec;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
@ -57,9 +50,6 @@ import java.io.IOException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -67,25 +57,28 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
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
|
||||
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 int ENCRYPTED_FORMAT_VERSION = 1;
|
||||
private static final int ENCRYPTED_FORMAT_LENGTH = 4*2; // version,salt
|
||||
private static final Logger log = LoggerFactory.getLogger(KeyStorage.class);
|
||||
|
||||
public enum KeyEntry {
|
||||
MSG_SIGNATURE("sig", Sig.KEY_ALGO),
|
||||
MSG_ENCRYPTION("enc", Encryption.ASYM_KEY_ALGO);
|
||||
SYM_ENCRYPTION("sym.p12", Encryption.SYM_KEY_ALGO, "sym"), // symmetric encryption for persistence
|
||||
MSG_SIGNATURE("sig.key", Sig.KEY_ALGO, "sig"),
|
||||
MSG_ENCRYPTION("enc.key", Encryption.ASYM_KEY_ALGO, "enc");
|
||||
|
||||
private final String fileName;
|
||||
private final String algorithm;
|
||||
private final String alias;
|
||||
|
||||
KeyEntry(String fileName, String algorithm) {
|
||||
KeyEntry(String fileName, String algorithm, String alias) {
|
||||
this.fileName = fileName;
|
||||
this.algorithm = algorithm;
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
@ -96,6 +89,10 @@ public class KeyStorage {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
@ -114,78 +111,40 @@ public class KeyStorage {
|
||||
}
|
||||
|
||||
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) {
|
||||
return new File(storageDir + "/" + keyEntry.getFileName() + ".key").exists();
|
||||
return new File(storageDir + "/" + keyEntry.getFileName()).exists();
|
||||
}
|
||||
|
||||
public KeyPair loadKeyPair(KeyEntry keyEntry, String password) throws IncorrectPasswordException {
|
||||
FileUtil.rollingBackup(storageDir, keyEntry.getFileName() + ".key", 20);
|
||||
// long now = System.currentTimeMillis();
|
||||
try {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm());
|
||||
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()];
|
||||
private byte[] loadKeyBytes(KeyEntry keyEntry, SecretKey secretKey) {
|
||||
File keyFile = new File(storageDir + "/" + keyEntry.getFileName());
|
||||
try (FileInputStream fis = new FileInputStream(keyFile.getPath())) {
|
||||
byte[] encodedKey = new byte[(int) keyFile.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) {
|
||||
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 {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm());
|
||||
byte[] encodedPrivateKey = loadKeyBytes(keyEntry, secretKey);
|
||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||
PublicKey publicKey;
|
||||
if (privateKey instanceof RSAPrivateCrtKey) {
|
||||
RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey;
|
||||
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent());
|
||||
@ -202,8 +161,6 @@ public class KeyStorage {
|
||||
} else {
|
||||
throw new RuntimeException("Unsupported key algo" + keyEntry.getAlgorithm());
|
||||
}
|
||||
|
||||
log.debug("load completed in {} msec", System.currentTimeMillis() - new Date().getTime());
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException 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);
|
||||
savePrivateKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName(), password);
|
||||
/**
|
||||
* Loads the password protected symmetric secret key for this key ring.
|
||||
*
|
||||
* @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())
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
storageDir.mkdirs();
|
||||
|
||||
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
|
||||
try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + name + ".key")) {
|
||||
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(key.getEncoded());
|
||||
byte[] keyBytes = pkcs8EncodedKeySpec.getEncoded();
|
||||
// Encrypt
|
||||
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
|
||||
try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + fileName)) {
|
||||
keyBytes = Encryption.encryptPayloadWithHmac(keyBytes, secretKey);
|
||||
}
|
||||
fos.write(keyBytes);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not save key " + name, e);
|
||||
throw new RuntimeException("Could not save key " + name, e);
|
||||
log.error("Could not save key " + fileName, 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.app.DevEnv;
|
||||
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.FileUtil;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
@ -34,6 +37,7 @@ import javax.inject.Named;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
@ -208,6 +212,8 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
private final File dir;
|
||||
private final PersistenceProtoResolver persistenceProtoResolver;
|
||||
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
||||
@Nullable
|
||||
private final KeyRing keyRing;
|
||||
private File storageFile;
|
||||
private T persistable;
|
||||
private String fileName;
|
||||
@ -228,10 +234,12 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
@Inject
|
||||
public PersistenceManager(@Named(Config.STORAGE_DIR) File dir,
|
||||
PersistenceProtoResolver persistenceProtoResolver,
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||
@Nullable KeyRing keyRing) {
|
||||
this.dir = checkDir(dir);
|
||||
this.persistenceProtoResolver = persistenceProtoResolver;
|
||||
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.");
|
||||
return null;
|
||||
}
|
||||
if (keyRing != null && !keyRing.isUnlocked()) {
|
||||
log.warn("Account is not open yet, ignoring getPersisted.");
|
||||
return null;
|
||||
}
|
||||
|
||||
readCalled.set(true);
|
||||
|
||||
@ -347,7 +359,21 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
|
||||
long ts = System.currentTimeMillis();
|
||||
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
|
||||
T persistableEnvelope = (T) persistenceProtoResolver.fromProto(proto);
|
||||
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) {
|
||||
if (!allServicesInitialized.get()) {
|
||||
log.warn("Application has not completed start up yet so we do not permit writing data to disk.");
|
||||
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;
|
||||
}
|
||||
|
||||
@ -457,7 +492,12 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
|
||||
fileOutputStream = new FileOutputStream(tempFile);
|
||||
|
||||
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
|
||||
// to not write through to physical media for at least a few seconds, but this is the best we can do.
|
||||
|
@ -64,6 +64,8 @@ public class ZipUtils {
|
||||
|
||||
// Close the zip entry.
|
||||
zos.closeEntry();
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,8 +107,8 @@ public class CoreAccountService {
|
||||
|
||||
public void changePassword(String password) {
|
||||
if (!isAccountOpen()) throw new IllegalStateException("Cannot change password on unopened account");
|
||||
keyStorage.saveKeyRing(keyRing, password);
|
||||
String oldPassword = this.password;
|
||||
keyStorage.saveKeyRing(keyRing, oldPassword, password);
|
||||
this.password = password;
|
||||
for (AccountServiceListener listener : listeners) listener.onPasswordChanged(oldPassword, password);
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import bisq.core.trade.TradableList;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.peers.PeerManager;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.KeyStorage;
|
||||
import bisq.common.file.CorruptedStorageFileHandler;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
@ -34,8 +36,9 @@ public class OpenOfferManagerTest {
|
||||
public void setUp() throws Exception {
|
||||
var corruptedStorageFileHandler = mock(CorruptedStorageFileHandler.class);
|
||||
var storageDir = Files.createTempDirectory("storage").toFile();
|
||||
persistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler);
|
||||
signedOfferPersistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler);
|
||||
var keyRing = new KeyRing(new KeyStorage(storageDir));
|
||||
persistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler, keyRing);
|
||||
signedOfferPersistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler, keyRing);
|
||||
coreContext = new CoreContext();
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.file.CorruptedStorageFileHandler;
|
||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||
|
||||
@ -59,6 +60,7 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||
private final String accountsFileName = "AltcoinPaymentAccounts";
|
||||
private final PersistenceProtoResolver persistenceProtoResolver;
|
||||
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
||||
private final KeyRing keyRing;
|
||||
|
||||
@Inject
|
||||
public AltCoinAccountsDataModel(User user,
|
||||
@ -67,7 +69,8 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||
TradeManager tradeManager,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
PersistenceProtoResolver persistenceProtoResolver,
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||
KeyRing keyRing) {
|
||||
this.user = user;
|
||||
this.preferences = preferences;
|
||||
this.openOfferManager = openOfferManager;
|
||||
@ -75,6 +78,7 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.persistenceProtoResolver = persistenceProtoResolver;
|
||||
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
||||
this.keyRing = keyRing;
|
||||
setChangeListener = change -> fillAndSortPaymentAccounts();
|
||||
}
|
||||
|
||||
@ -150,12 +154,12 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
|
||||
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
|
||||
.filter(paymentAccount -> paymentAccount instanceof AssetAccount)
|
||||
.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) {
|
||||
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
|
||||
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||
}
|
||||
|
||||
public int getNumPaymentAccounts() {
|
||||
|
@ -32,6 +32,7 @@ import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.file.CorruptedStorageFileHandler;
|
||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||
|
||||
@ -60,6 +61,7 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||
private final String accountsFileName = "FiatPaymentAccounts";
|
||||
private final PersistenceProtoResolver persistenceProtoResolver;
|
||||
private final CorruptedStorageFileHandler corruptedStorageFileHandler;
|
||||
private final KeyRing keyRing;
|
||||
|
||||
@Inject
|
||||
public FiatAccountsDataModel(User user,
|
||||
@ -68,7 +70,8 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||
TradeManager tradeManager,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
PersistenceProtoResolver persistenceProtoResolver,
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||
KeyRing keyRing) {
|
||||
this.user = user;
|
||||
this.preferences = preferences;
|
||||
this.openOfferManager = openOfferManager;
|
||||
@ -76,6 +79,7 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.persistenceProtoResolver = persistenceProtoResolver;
|
||||
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
|
||||
this.keyRing = keyRing;
|
||||
setChangeListener = change -> fillAndSortPaymentAccounts();
|
||||
}
|
||||
|
||||
@ -153,12 +157,12 @@ class FiatAccountsDataModel extends ActivatableDataModel {
|
||||
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
|
||||
.filter(paymentAccount -> !(paymentAccount instanceof AssetAccount))
|
||||
.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) {
|
||||
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
|
||||
GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing);
|
||||
}
|
||||
|
||||
public int getNumPaymentAccounts() {
|
||||
|
@ -54,6 +54,7 @@ import bisq.network.p2p.P2PService;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.DevEnv;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.file.CorruptedStorageFileHandler;
|
||||
import bisq.common.persistence.PersistenceManager;
|
||||
import bisq.common.proto.persistable.PersistableEnvelope;
|
||||
@ -203,11 +204,12 @@ public class GUIUtil {
|
||||
Preferences preferences,
|
||||
Stage stage,
|
||||
PersistenceProtoResolver persistenceProtoResolver,
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||
KeyRing keyRing) {
|
||||
if (!accounts.isEmpty()) {
|
||||
String directory = getDirectoryFromChooser(preferences, stage);
|
||||
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);
|
||||
persistenceManager.initialize(paymentAccounts, fileName, PersistenceManager.Source.PRIVATE_LOW_PRIO);
|
||||
persistenceManager.persistNow(() -> {
|
||||
@ -227,7 +229,8 @@ public class GUIUtil {
|
||||
Preferences preferences,
|
||||
Stage stage,
|
||||
PersistenceProtoResolver persistenceProtoResolver,
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler,
|
||||
KeyRing keyRing) {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
File initDir = new File(preferences.getDirectoryChooserPath());
|
||||
if (initDir.isDirectory()) {
|
||||
@ -240,7 +243,7 @@ public class GUIUtil {
|
||||
if (Paths.get(path).getFileName().toString().equals(fileName)) {
|
||||
String directory = Paths.get(path).getParent().toString();
|
||||
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 -> {
|
||||
StringBuilder msg = new StringBuilder();
|
||||
HashSet<PaymentAccount> paymentAccounts = new HashSet<>();
|
||||
|
@ -131,7 +131,7 @@ public class P2PNetworkLoad extends Metric implements MessageListener, SetupList
|
||||
CorePersistenceProtoResolver persistenceProtoResolver = new CorePersistenceProtoResolver(null, null, networkProtoResolver);
|
||||
DefaultSeedNodeRepository seedNodeRepository = new DefaultSeedNodeRepository(config);
|
||||
PeerManager peerManager = new PeerManager(networkNode, seedNodeRepository, new ClockWatcher(),
|
||||
new PersistenceManager<>(torHiddenServiceDir, persistenceProtoResolver, corruptedStorageFileHandler), maxConnections);
|
||||
new PersistenceManager<>(torHiddenServiceDir, persistenceProtoResolver, corruptedStorageFileHandler, null), maxConnections);
|
||||
|
||||
// init file storage
|
||||
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
|
||||
TradeStatistics3Store tradeStatistics3Store = new TradeStatistics3Store();
|
||||
PersistenceManager<TradeStatistics3Store> tradeStatistics3PersistenceManager = new PersistenceManager<>(dir,
|
||||
persistenceProtoResolver, null);
|
||||
persistenceProtoResolver, null, null);
|
||||
tradeStatistics3PersistenceManager.initialize(tradeStatistics3Store,
|
||||
tradeStatistics3Store.getDefaultStorageFileName() + networkPostfix,
|
||||
PersistenceManager.Source.NETWORK);
|
||||
@ -133,7 +133,7 @@ public abstract class P2PSeedNodeSnapshotBase extends Metric implements MessageL
|
||||
|
||||
AccountAgeWitnessStore accountAgeWitnessStore = new AccountAgeWitnessStore();
|
||||
PersistenceManager<AccountAgeWitnessStore> accountAgeWitnessPersistenceManager = new PersistenceManager<>(dir,
|
||||
persistenceProtoResolver, null);
|
||||
persistenceProtoResolver, null, null);
|
||||
accountAgeWitnessPersistenceManager.initialize(accountAgeWitnessStore,
|
||||
accountAgeWitnessStore.getDefaultStorageFileName() + networkPostfix,
|
||||
PersistenceManager.Source.NETWORK);
|
||||
|
@ -30,6 +30,8 @@ import bisq.network.p2p.peers.peerexchange.PeerList;
|
||||
import bisq.network.p2p.seed.SeedNodeRepository;
|
||||
|
||||
import bisq.common.ClockWatcher;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.KeyStorage;
|
||||
import bisq.common.file.CorruptedStorageFileHandler;
|
||||
import bisq.common.persistence.PersistenceManager;
|
||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||
@ -64,7 +66,7 @@ public class MockNode {
|
||||
this.maxConnections = maxConnections;
|
||||
networkNode = mock(NetworkNode.class);
|
||||
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);
|
||||
connections = new HashSet<>();
|
||||
when(networkNode.getAllConnections()).thenReturn(connections);
|
||||
|
Loading…
Reference in New Issue
Block a user