mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-24 07:30:54 -04:00
Add API functions to initialize Haveno account (#216)
Co-authored-by: woodser@protonmail.com
This commit is contained in:
parent
dc4692d97a
commit
e3b9a9962b
81 changed files with 2755 additions and 1660 deletions
|
@ -114,6 +114,7 @@ public class Config {
|
|||
public static final String BTC_MIN_TX_FEE = "btcMinTxFee";
|
||||
public static final String BTC_FEES_TS = "bitcoinFeesTs";
|
||||
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
|
||||
public static final String PASSWORD_REQUIRED = "passwordRequired";
|
||||
|
||||
// Default values for certain options
|
||||
public static final int UNSPECIFIED_PORT = -1;
|
||||
|
@ -190,6 +191,7 @@ public class Config {
|
|||
public final boolean preventPeriodicShutdownAtSeedNode;
|
||||
public final boolean republishMailboxEntries;
|
||||
public final boolean bypassMempoolValidation;
|
||||
public final boolean passwordRequired;
|
||||
|
||||
// Properties derived from options but not exposed as options themselves
|
||||
public final File torDir;
|
||||
|
@ -579,6 +581,13 @@ public class Config {
|
|||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> passwordRequiredOpt =
|
||||
parser.accepts(PASSWORD_REQUIRED,
|
||||
"Requires a password for creating a Haveno account")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
try {
|
||||
CompositeOptionSet options = new CompositeOptionSet();
|
||||
|
||||
|
@ -686,6 +695,7 @@ public class Config {
|
|||
this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt);
|
||||
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
|
||||
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
||||
this.passwordRequired = options.valueOf(passwordRequiredOpt);
|
||||
} catch (OptionException ex) {
|
||||
throw new ConfigException("problem parsing option '%s': %s",
|
||||
ex.options().get(0),
|
||||
|
|
|
@ -54,11 +54,13 @@ public class Encryption {
|
|||
public static final String ASYM_KEY_ALGO = "RSA";
|
||||
private static final String ASYM_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1PADDING";
|
||||
|
||||
private static final String SYM_KEY_ALGO = "AES";
|
||||
public static final String SYM_KEY_ALGO = "AES";
|
||||
private static final String SYM_CIPHER = "AES";
|
||||
|
||||
private static final String HMAC = "HmacSHA256";
|
||||
|
||||
public static final String HMAC_ERROR_MSG = "Hmac does not match.";
|
||||
|
||||
public static KeyPair generateKeyPair() {
|
||||
long ts = System.currentTimeMillis();
|
||||
try {
|
||||
|
@ -101,11 +103,6 @@ public class Encryption {
|
|||
return new SecretKeySpec(secretKeyBytes, 0, secretKeyBytes.length, SYM_KEY_ALGO);
|
||||
}
|
||||
|
||||
public static byte[] getSecretKeyBytes(SecretKey secretKey) {
|
||||
return secretKey.getEncoded();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Hmac
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -179,7 +176,7 @@ public class Encryption {
|
|||
if (verifyHmac(Hex.decode(payloadAsHex), Hex.decode(hmacAsHex), secretKey)) {
|
||||
return Hex.decode(payloadAsHex);
|
||||
} else {
|
||||
throw new CryptoException("Hmac does not match.");
|
||||
throw new CryptoException(HMAC_ERROR_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.common.crypto;
|
||||
|
||||
public class IncorrectPasswordException extends Exception {
|
||||
public IncorrectPasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -26,27 +26,93 @@ import lombok.EqualsAndHashCode;
|
|||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public final class KeyRing {
|
||||
private final KeyPair signatureKeyPair;
|
||||
private final KeyPair encryptionKeyPair;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
|
||||
private final KeyStorage keyStorage;
|
||||
|
||||
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
|
||||
public KeyRing(KeyStorage keyStorage) {
|
||||
if (keyStorage.allKeyFilesExist()) {
|
||||
signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE);
|
||||
encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION);
|
||||
} else {
|
||||
// First time we create key pairs
|
||||
signatureKeyPair = Sig.generateKeyPair();
|
||||
encryptionKeyPair = Encryption.generateKeyPair();
|
||||
keyStorage.saveKeyRing(this);
|
||||
this(keyStorage, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public KeyRing(KeyStorage keyStorage, String password, boolean generateKeys) {
|
||||
this.keyStorage = keyStorage;
|
||||
try {
|
||||
unlockKeys(password, generateKeys);
|
||||
} catch(IncorrectPasswordException ex) {
|
||||
// no action
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
boolean isUnlocked = this.signatureKeyPair != null
|
||||
&& this.encryptionKeyPair != null
|
||||
&& this.pubKeyRing != null;
|
||||
return isUnlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the keyring disabling access to the keys until unlock is called.
|
||||
* If the keys are never persisted then the keys are lost and will be regenerated.
|
||||
*/
|
||||
public void lockKeys() {
|
||||
signatureKeyPair = null;
|
||||
encryptionKeyPair = 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);
|
||||
if (signatureKeyPair != null && encryptionKeyPair != null) pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
||||
} else if (generateKeys) {
|
||||
generateKeys(password);
|
||||
}
|
||||
return isUnlocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
signatureKeyPair = Sig.generateKeyPair();
|
||||
encryptionKeyPair = Encryption.generateKeyPair();
|
||||
pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
||||
keyStorage.saveKeyRing(this, password);
|
||||
}
|
||||
|
||||
// Don't print keys for security reasons
|
||||
|
|
|
@ -20,11 +20,19 @@ 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.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -39,6 +47,9 @@ 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;
|
||||
|
@ -46,6 +57,7 @@ import java.io.IOException;
|
|||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
@ -58,6 +70,11 @@ import static bisq.common.util.Preconditions.checkDir;
|
|||
@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
|
||||
|
||||
public enum KeyEntry {
|
||||
MSG_SIGNATURE("sig", Sig.KEY_ALGO),
|
||||
|
@ -104,7 +121,7 @@ public class KeyStorage {
|
|||
return new File(storageDir + "/" + keyEntry.getFileName() + ".key").exists();
|
||||
}
|
||||
|
||||
public KeyPair loadKeyPair(KeyEntry keyEntry) {
|
||||
public KeyPair loadKeyPair(KeyEntry keyEntry, String password) throws IncorrectPasswordException {
|
||||
FileUtil.rollingBackup(storageDir, keyEntry.getFileName() + ".key", 20);
|
||||
// long now = System.currentTimeMillis();
|
||||
try {
|
||||
|
@ -118,9 +135,53 @@ public class KeyStorage {
|
|||
//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 || ce.getMessage() == Encryption.HMAC_ERROR_MSG)
|
||||
throw new IncorrectPasswordException("Incorrect password");
|
||||
else
|
||||
throw ce;
|
||||
}
|
||||
}
|
||||
|
||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
|
||||
privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||
} catch (InvalidKeySpecException | IOException e) {
|
||||
} 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);
|
||||
}
|
||||
|
@ -150,20 +211,44 @@ public class KeyStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public void saveKeyRing(KeyRing keyRing) {
|
||||
savePrivateKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName());
|
||||
savePrivateKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName());
|
||||
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);
|
||||
}
|
||||
|
||||
private void savePrivateKey(PrivateKey privateKey, String name) {
|
||||
private void savePrivateKey(PrivateKey privateKey, String name, String password) {
|
||||
if (!storageDir.exists())
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
storageDir.mkdir();
|
||||
storageDir.mkdirs();
|
||||
|
||||
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
|
||||
try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + name + ".key")) {
|
||||
fos.write(pkcs8EncodedKeySpec.getEncoded());
|
||||
} catch (IOException e) {
|
||||
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);
|
||||
}
|
||||
fos.write(keyBytes);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not save key " + name, e);
|
||||
throw new RuntimeException("Could not save key " + name, e);
|
||||
}
|
||||
|
|
|
@ -3,17 +3,22 @@ package bisq.common.crypto;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
/**
|
||||
* Allows User's static PubKeyRing to be injected into constructors without having to
|
||||
* open the account yet. Once its opened, PubKeyRingProvider will return non-null PubKeyRing.
|
||||
* Originally used via bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class);
|
||||
*/
|
||||
public class PubKeyRingProvider implements Provider<PubKeyRing> {
|
||||
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final KeyRing keyRing;
|
||||
|
||||
@Inject
|
||||
public PubKeyRingProvider(KeyRing keyRing) {
|
||||
pubKeyRing = keyRing.getPubKeyRing();
|
||||
this.keyRing = keyRing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing get() {
|
||||
return pubKeyRing;
|
||||
return keyRing.getPubKeyRing();
|
||||
}
|
||||
}
|
||||
|
|
90
common/src/main/java/bisq/common/crypto/ScryptUtil.java
Normal file
90
common/src/main/java/bisq/common/crypto/ScryptUtil.java
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.common.crypto;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
import org.bitcoinj.wallet.Protos;
|
||||
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
//TODO: Borrowed form BitcoinJ/Lighthouse. Remove Protos dependency, check complete code logic.
|
||||
public class ScryptUtil {
|
||||
private static final Logger log = LoggerFactory.getLogger(ScryptUtil.class);
|
||||
|
||||
public interface DeriveKeyResultHandler {
|
||||
void handleResult(KeyParameter aesKey);
|
||||
}
|
||||
|
||||
public static KeyCrypterScrypt getKeyCrypterScrypt() {
|
||||
return getKeyCrypterScrypt(KeyCrypterScrypt.randomSalt());
|
||||
}
|
||||
|
||||
public static KeyCrypterScrypt getKeyCrypterScrypt(byte[] salt) {
|
||||
Protos.ScryptParameters scryptParameters = Protos.ScryptParameters.newBuilder()
|
||||
.setP(6)
|
||||
.setR(8)
|
||||
.setN(32768)
|
||||
.setSalt(ByteString.copyFrom(salt))
|
||||
.build();
|
||||
return new KeyCrypterScrypt(scryptParameters);
|
||||
}
|
||||
|
||||
public static KeyParameter deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password) {
|
||||
try {
|
||||
log.debug("Doing key derivation");
|
||||
long start = System.currentTimeMillis();
|
||||
KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
log.debug("Key derivation took {} msec", duration);
|
||||
return aesKey;
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
log.error("Key derivation failed. " + t.getMessage());
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
public static void deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password, DeriveKeyResultHandler resultHandler) {
|
||||
Utilities.getThreadPoolExecutor("ScryptUtil:deriveKeyWithScrypt-%d", 1, 2, 5L).submit(() -> {
|
||||
try {
|
||||
KeyParameter aesKey = deriveKeyWithScrypt(keyCrypterScrypt, password);
|
||||
UserThread.execute(() -> {
|
||||
try {
|
||||
resultHandler.handleResult(aesKey);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
log.error("Executing task failed. " + t.getMessage());
|
||||
throw t;
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
log.error("Executing task failed. " + t.getMessage());
|
||||
throw t;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -114,7 +114,6 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
// We don't know from which thread we are called so we map to user thread
|
||||
UserThread.execute(() -> {
|
||||
if (doShutdown) {
|
||||
|
@ -382,6 +381,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!initCalled.get()) {
|
||||
log.warn("requestPersistence() called before init. Ignoring request");
|
||||
return;
|
||||
}
|
||||
|
||||
persistenceRequested = true;
|
||||
|
||||
// If we have not initialized yet we postpone the start of the timer and call maybeStartTimerForPersistence at
|
||||
|
|
128
common/src/main/java/bisq/common/util/ZipUtils.java
Normal file
128
common/src/main/java/bisq/common/util/ZipUtils.java
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
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.
|
||||
*/
|
||||
public static void zipDirToStream(File dir, OutputStream out, int bufferSize) throws Exception {
|
||||
|
||||
// Get all files in directory and subdirectories.
|
||||
ArrayList<String> fileList = new ArrayList<>();
|
||||
getFilesRecursive(dir, fileList);
|
||||
try (ZipOutputStream zos = new ZipOutputStream(out)) {
|
||||
for (String filePath : fileList) {
|
||||
log.info("Compressing: " + filePath);
|
||||
|
||||
// Creates a zip entry.
|
||||
String name = filePath.substring(dir.getAbsolutePath().length() + 1);
|
||||
|
||||
ZipEntry zipEntry = new ZipEntry(name);
|
||||
zos.putNextEntry(zipEntry);
|
||||
|
||||
// Read file content and write to zip output stream.
|
||||
try (FileInputStream fis = new FileInputStream(filePath)) {
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int length;
|
||||
while ((length = fis.read(buffer)) > 0) {
|
||||
zos.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
// Close the zip entry.
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files list from the directory recursive to the subdirectory.
|
||||
*/
|
||||
public static void getFilesRecursive(File directory, List<String> fileList) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
if (file.isFile()) {
|
||||
fileList.add(file.getAbsolutePath());
|
||||
} else {
|
||||
getFilesRecursive(file, fileList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static void unzipToDir(File dir, InputStream inputStream, int bufferSize) throws Exception {
|
||||
try (ZipInputStream zipStream = new ZipInputStream(inputStream)) {
|
||||
ZipEntry entry;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int count;
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(dir, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
file.mkdirs();
|
||||
} else {
|
||||
|
||||
// Make sure folder exists.
|
||||
file.getParentFile().mkdirs();
|
||||
|
||||
log.info("Unzipped file: " + file.getAbsolutePath());
|
||||
// Don't overwrite the current logs
|
||||
if ("bisq.log".equals(file.getName())) {
|
||||
file = new File(file.getParent() + "/" + "bisq.backup.log");
|
||||
log.info("Unzipped logfile to backup path: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOutput = new FileOutputStream(file)) {
|
||||
while ((count = zipStream.read(buffer)) != -1) {
|
||||
fileOutput.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
zipStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue