diff --git a/common/src/main/java/bisq/common/crypto/Encryption.java b/common/src/main/java/bisq/common/crypto/Encryption.java index e6e0f58ffe..dabde8c0dc 100644 --- a/common/src/main/java/bisq/common/crypto/Encryption.java +++ b/common/src/main/java/bisq/common/crypto/Encryption.java @@ -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"); diff --git a/common/src/main/java/bisq/common/crypto/KeyRing.java b/common/src/main/java/bisq/common/crypto/KeyRing.java index 49b89bbc24..d9bc3dabd4 100644 --- a/common/src/main/java/bisq/common/crypto/KeyRing.java +++ b/common/src/main/java/bisq/common/crypto/KeyRing.java @@ -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() + '}'; diff --git a/common/src/main/java/bisq/common/crypto/KeyStorage.java b/common/src/main/java/bisq/common/crypto/KeyStorage.java index c39089a05a..6484dcb0f5 100644 --- a/common/src/main/java/bisq/common/crypto/KeyStorage.java +++ b/common/src/main/java/bisq/common/crypto/KeyStorage.java @@ -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); } } } diff --git a/common/src/main/java/bisq/common/persistence/PersistenceManager.java b/common/src/main/java/bisq/common/persistence/PersistenceManager.java index 37f7fe4735..0095b6c780 100644 --- a/common/src/main/java/bisq/common/persistence/PersistenceManager.java +++ b/common/src/main/java/bisq/common/persistence/PersistenceManager.java @@ -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 { 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 { @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 { 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 { 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 { 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 { 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. diff --git a/common/src/main/java/bisq/common/util/ZipUtils.java b/common/src/main/java/bisq/common/util/ZipUtils.java index 0a1299f597..e47087b8ba 100644 --- a/common/src/main/java/bisq/common/util/ZipUtils.java +++ b/common/src/main/java/bisq/common/util/ZipUtils.java @@ -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. diff --git a/core/src/main/java/bisq/core/api/CoreAccountService.java b/core/src/main/java/bisq/core/api/CoreAccountService.java index 63b8cf07c5..95ff2593c6 100644 --- a/core/src/main/java/bisq/core/api/CoreAccountService.java +++ b/core/src/main/java/bisq/core/api/CoreAccountService.java @@ -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 listeners = new ArrayList(); - + @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 consume, Consumer 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(); diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java index 65d290824d..67a93ebec5 100644 --- a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -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(); } diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java index af5d2fd060..7ca9b43993 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsDataModel.java @@ -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 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() { diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java index 6f738f42f5..f99590e87e 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java @@ -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 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() { diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index f8335d39fc..3ee66a4a98 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -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 persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler); + PersistenceManager 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 persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler); + PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, keyRing); persistenceManager.readPersisted(fileName, persisted -> { StringBuilder msg = new StringBuilder(); HashSet paymentAccounts = new HashSet<>(); diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java index 565ff694f9..f17aed0a2a 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java @@ -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(() -> { diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java index 3c5d371bb6..c59bb358f6 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java @@ -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 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 accountAgeWitnessPersistenceManager = new PersistenceManager<>(dir, - persistenceProtoResolver, null); + persistenceProtoResolver, null, null); accountAgeWitnessPersistenceManager.initialize(accountAgeWitnessStore, accountAgeWitnessStore.getDefaultStorageFileName() + networkPostfix, PersistenceManager.Source.NETWORK); diff --git a/p2p/src/test/java/bisq/network/p2p/MockNode.java b/p2p/src/test/java/bisq/network/p2p/MockNode.java index b567168a89..089152ca97 100644 --- a/p2p/src/test/java/bisq/network/p2p/MockNode.java +++ b/p2p/src/test/java/bisq/network/p2p/MockNode.java @@ -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);