Encrypt persisted data using password protected symmetric key (#279)

This commit is contained in:
duriancrepe 2022-04-12 04:49:30 -07:00 committed by GitHub
parent 75c66ee43f
commit 5b38eab716
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 254 additions and 169 deletions

View file

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

View file

@ -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,13 +38,14 @@ public final class KeyRing {
private final KeyStorage keyStorage;
private SecretKey symmetricKey;
private KeyPair signatureKeyPair;
private KeyPair encryptionKeyPair;
private PubKeyRing pubKeyRing;
/**
* Creates the KeyRing. Unlocks if not encrypted. Does not generate keys.
*
*
* @param keyStorage Persisted storage
*/
@Inject
@ -52,7 +55,7 @@ public final class KeyRing {
/**
* Creates KeyRing with a password. Attempts to generate keys if they don't exist.
*
*
* @param keyStorage Persisted storage
* @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.
@ -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,21 +84,23 @@ public final class KeyRing {
public void lockKeys() {
signatureKeyPair = null;
encryptionKeyPair = null;
symmetricKey = null;
pubKeyRing = null;
}
/**
* Unlocks the keyring with a given password if required. If the keyring is already
* unlocked, do nothing.
*
*
* @param password Decrypts the or encrypts newly generated keys with the given password.
* @return Whether KeyRing is unlocked
*/
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);
@ -104,22 +110,24 @@ public final class KeyRing {
/**
* 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.
*/
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() +
'}';

View file

@ -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();
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(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;
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) {
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")) {
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
keyBytes = Encryption.encryptPayloadWithHmac(keyBytes, secretKey);
}
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(key.getEncoded());
byte[] keyBytes = pkcs8EncodedKeySpec.getEncoded();
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);
}
}
}

View file

@ -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.");
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;
}
@ -457,7 +492,12 @@ public class PersistenceManager<T extends PersistableEnvelope> {
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
// to not write through to physical media for at least a few seconds, but this is the best we can do.

View file

@ -35,7 +35,7 @@ public class ZipUtils {
/**
* Zips directory into the output stream. Empty directories are not included.
*
*
* @param dir The directory to create the zip from.
* @param out The stream to write to.
*/
@ -64,6 +64,8 @@ public class ZipUtils {
// Close the zip entry.
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.
* Existing files are preserved.
*
*
* @param dir The directory to write to.
* @param inputStream The raw stream assumed to be in zip format.
* @param bufferSize The buffer used to read from efficiently.

View file

@ -50,15 +50,15 @@ import lombok.extern.slf4j.Slf4j;
@Singleton
@Slf4j
public class CoreAccountService {
private final Config config;
private final KeyStorage keyStorage;
private final KeyRing keyRing;
@Getter
private String password;
private List<AccountServiceListener> listeners = new ArrayList<AccountServiceListener>();
@Inject
public CoreAccountService(Config config,
KeyStorage keyStorage,
@ -67,34 +67,34 @@ public class CoreAccountService {
this.keyStorage = keyStorage;
this.keyRing = keyRing;
}
public void addListener(AccountServiceListener listener) {
listeners.add(listener);
}
public boolean removeListener(AccountServiceListener listener) {
return listeners.remove(listener);
}
public boolean accountExists() {
return keyStorage.allKeyFilesExist(); // public and private key pair indicate the existence of the account
}
public boolean isAccountOpen() {
return keyRing.isUnlocked() && accountExists();
}
public void checkAccountOpen() {
checkState(isAccountOpen(), "Account not open");
}
public void createAccount(String password) {
if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists");
keyRing.generateKeys(password);
this.password = password;
for (AccountServiceListener listener : listeners) listener.onAccountCreated();
}
public void openAccount(String password) throws IncorrectPasswordException {
if (!accountExists()) throw new IllegalStateException("Cannot open account if account does not exist");
if (keyRing.unlockKeys(password, false)) {
@ -104,21 +104,21 @@ public class CoreAccountService {
throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen");
}
}
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);
}
public void closeAccount() {
if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account");
keyRing.lockKeys(); // closed account means the keys are locked
for (AccountServiceListener listener : listeners) listener.onAccountClosed();
}
public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) {
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 {
if (accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
File dataDir = new File(config.appDataDir.getPath());
ZipUtils.unzipToDir(dataDir, inputStream, bufferSize);
for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown);
}
public void deleteAccount(Runnable onShutdown) {
try {
keyRing.lockKeys();

View file

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

View file

@ -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() {

View file

@ -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() {

View file

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

View file

@ -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(() -> {

View file

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

View file

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