lease = mdPool.get();
+ lease.get().reset();
+ return lease;
+ }
+
+ /**
+ * Creates a new MessageDigest.
+ *
+ * @return New MessageDigest instance
+ * @deprecated Use {@link #instance()}
+ */
+ @Deprecated
public MessageDigest get() {
- final MessageDigest result = threadLocal.get();
+ final MessageDigest result = createMessageDigest();
result.reset();
return result;
}
diff --git a/src/main/java/org/cryptomator/cryptolib/common/ObjectPool.java b/src/main/java/org/cryptomator/cryptolib/common/ObjectPool.java
new file mode 100644
index 0000000..00de730
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/common/ObjectPool.java
@@ -0,0 +1,71 @@
+package org.cryptomator.cryptolib.common;
+
+import java.lang.ref.WeakReference;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.function.Supplier;
+
+/**
+ * A simple object pool for resources that are expensive to create but are needed frequently.
+ *
+ * Example Usage:
+ *
{@code
+ * Supplier fooFactory = () -> new Foo();
+ * ObjectPool fooPool = new ObjectPool(fooFactory);
+ * try (ObjectPool.Lease lease = fooPool.get()) { // attempts to get a pooled Foo or invokes factory
+ * lease.get().foo(); // exclusively use Foo instance
+ * } // releases instance back to the pool when done
+ * }
+ *
+ * @param Type of the pooled objects
+ */
+public class ObjectPool {
+
+ private final Queue> returnedInstances;
+ private final Supplier factory;
+
+ public ObjectPool(Supplier factory) {
+ this.returnedInstances = new ConcurrentLinkedQueue<>();
+ this.factory = factory;
+ }
+
+ public Lease get() {
+ WeakReference ref;
+ while ((ref = returnedInstances.poll()) != null) {
+ T cached = ref.get();
+ if (cached != null) {
+ return new Lease<>(this, cached);
+ }
+ }
+ return new Lease<>(this, factory.get());
+ }
+
+ /**
+ * A holder for resource leased from an {@link ObjectPool}.
+ * This is basically an {@link AutoCloseable autocloseable} {@link Supplier} that is intended to be used
+ * via try-with-resource blocks.
+ *
+ * @param Type of the leased instance
+ */
+ public static class Lease implements AutoCloseable, Supplier {
+
+ private final ObjectPool pool;
+ private T obj;
+
+ private Lease(ObjectPool pool, T obj) {
+ this.pool = pool;
+ this.obj = obj;
+ }
+
+ public T get() {
+ return obj;
+ }
+
+ @Override
+ public void close() {
+ pool.returnedInstances.add(new WeakReference<>(obj));
+ obj = null;
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java b/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java
index 4d35aaf..05134a0 100644
--- a/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java
+++ b/src/main/java/org/cryptomator/cryptolib/common/Scrypt.java
@@ -72,21 +72,21 @@ public static byte[] scrypt(byte[] passphrase, byte[] salt, int costParam, int b
throw new IllegalArgumentException("Parameter r is too large");
}
- try (DestroyableSecretKey key = new DestroyableSecretKey(passphrase, "HmacSHA256")) {
- Mac mac = MacSupplier.HMAC_SHA256.withKey(key);
+ try (DestroyableSecretKey key = new DestroyableSecretKey(passphrase, "HmacSHA256");
+ ObjectPool.Lease mac = MacSupplier.HMAC_SHA256.keyed(key)) {
byte[] DK = new byte[keyLengthInBytes];
byte[] B = new byte[128 * blockSize * P];
byte[] XY = new byte[256 * blockSize];
byte[] V = new byte[128 * blockSize * costParam];
- pbkdf2(mac, salt, 1, B, P * 128 * blockSize);
+ pbkdf2(mac.get(), salt, 1, B, P * 128 * blockSize);
for (int i = 0; i < P; i++) {
smix(B, i * 128 * blockSize, blockSize, costParam, V, XY);
}
- pbkdf2(mac, B, 1, DK, keyLengthInBytes);
+ pbkdf2(mac.get(), B, 1, DK, keyLengthInBytes);
return DK;
}
diff --git a/src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java b/src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java
index 891ef83..093af59 100644
--- a/src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java
+++ b/src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java
@@ -3,6 +3,7 @@
import com.google.common.base.Throwables;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.ObjectPool;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
@@ -26,8 +27,9 @@ public int requiredSecretBytes() {
public byte[] encrypt(byte[] secret, byte[] plaintext) {
try (DestroyableSecretKey key = new DestroyableSecretKey(secret, 0, GCM_KEY_SIZE, "AES")) {
byte[] nonce = Arrays.copyOfRange(secret, GCM_KEY_SIZE, GCM_KEY_SIZE + GCM_NONCE_SIZE);
- Cipher cipher = CipherSupplier.AES_GCM.forEncryption(key, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
- return cipher.doFinal(plaintext);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ return cipher.get().doFinal(plaintext);
+ }
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Unexpected exception during GCM decryption.", e);
}
@@ -37,8 +39,9 @@ public byte[] encrypt(byte[] secret, byte[] plaintext) {
public byte[] decrypt(byte[] secret, byte[] ciphertext) throws AEADBadTagException {
try (DestroyableSecretKey key = new DestroyableSecretKey(secret, 0, GCM_KEY_SIZE, "AES")) {
byte[] nonce = Arrays.copyOfRange(secret, GCM_KEY_SIZE, GCM_KEY_SIZE + GCM_NONCE_SIZE);
- Cipher cipher = CipherSupplier.AES_GCM.forDecryption(key, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
- return cipher.doFinal(ciphertext);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.decryptionCipher(key, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ return cipher.get().doFinal(ciphertext);
+ }
} catch (IllegalBlockSizeException | BadPaddingException e) {
Throwables.throwIfInstanceOf(e, AEADBadTagException.class);
throw new IllegalStateException("Unexpected exception during GCM decryption.", e);
diff --git a/src/main/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunction.java b/src/main/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunction.java
index f7a6b95..0b8ecf0 100644
--- a/src/main/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunction.java
+++ b/src/main/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunction.java
@@ -1,6 +1,7 @@
package org.cryptomator.cryptolib.ecies;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
+import org.cryptomator.cryptolib.common.ObjectPool;
import java.math.BigInteger;
import java.nio.ByteBuffer;
@@ -32,8 +33,8 @@ public interface KeyDerivationFunction {
* @return key data
*/
static byte[] ansiX963sha256Kdf(byte[] sharedSecret, byte[] sharedInfo, int keyDataLen) {
- MessageDigest digest = MessageDigestSupplier.SHA256.get(); // max input length is 2^64 - 1, see https://doi.org/10.6028/NIST.SP.800-56Cr2, Table 1
- int hashLen = digest.getDigestLength();
+ // max input length is 2^64 - 1, see https://doi.org/10.6028/NIST.SP.800-56Cr2, Table 1
+ int hashLen = 32; // fixed digest length for SHA-256 in bytes
// These two checks must be performed according to spec. However with 32 bit integers, we can't exceed any limits anyway:
assert BigInteger.valueOf(4L + sharedSecret.length + sharedInfo.length).compareTo(BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE)) < 0 : "input larger than hashmaxlen";
@@ -43,15 +44,15 @@ static byte[] ansiX963sha256Kdf(byte[] sharedSecret, byte[] sharedInfo, int keyD
assert ByteOrder.BIG_ENDIAN.equals(counter.order());
int n = (keyDataLen + hashLen - 1) / hashLen;
byte[] buffer = new byte[n * hashLen];
- try {
+ try (ObjectPool.Lease sha256 = MessageDigestSupplier.SHA256.instance()) {
for (int i = 0; i < n; i++) {
- digest.update(sharedSecret);
+ sha256.get().update(sharedSecret);
counter.clear();
counter.putInt(i + 1);
counter.flip();
- digest.update(counter);
- digest.update(sharedInfo);
- digest.digest(buffer, i * hashLen, hashLen);
+ sha256.get().update(counter);
+ sha256.get().update(sharedInfo);
+ sha256.get().digest(buffer, i * hashLen, hashLen);
}
return Arrays.copyOf(buffer, keyDataLen);
} catch (DigestException e) {
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/Constants.java b/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
index e4cc9d5..4662fae 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
@@ -10,6 +10,9 @@
final class Constants {
+ private Constants() {
+ }
+
static final String CONTENT_ENC_ALG = "AES";
static final int NONCE_SIZE = 16;
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
index cad0d0b..004ff10 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
@@ -15,6 +15,7 @@
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MacSupplier;
+import org.cryptomator.cryptolib.common.ObjectPool;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -107,17 +108,18 @@ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long ch
// nonce:
byte[] nonce = new byte[NONCE_SIZE];
random.nextBytes(nonce);
+ ciphertextChunk.put(nonce);
// payload:
- final Cipher cipher = CipherSupplier.AES_CTR.forEncryption(fk, new IvParameterSpec(nonce));
- ciphertextChunk.put(nonce);
- assert ciphertextChunk.remaining() >= cipher.getOutputSize(cleartextChunk.remaining()) + MAC_SIZE;
- int bytesEncrypted = cipher.doFinal(cleartextChunk, ciphertextChunk);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_CTR.encryptionCipher(fk, new IvParameterSpec(nonce))) {
+ assert ciphertextChunk.remaining() >= cipher.get().getOutputSize(cleartextChunk.remaining()) + MAC_SIZE;
+ cipher.get().doFinal(cleartextChunk, ciphertextChunk);
+ }
// mac:
- final ByteBuffer ciphertextBuf = ciphertextChunk.asReadOnlyBuffer();
- ciphertextBuf.position(NONCE_SIZE).limit(NONCE_SIZE + bytesEncrypted);
- byte[] authenticationCode = calcChunkMac(headerNonce, chunkNumber, nonce, ciphertextBuf);
+ ByteBuffer nonceAndPayload = ciphertextChunk.duplicate();
+ nonceAndPayload.flip();
+ byte[] authenticationCode = calcChunkMac(headerNonce, chunkNumber, nonceAndPayload);
assert authenticationCode.length == MAC_SIZE;
ciphertextChunk.put(authenticationCode);
} catch (ShortBufferException e) {
@@ -134,18 +136,17 @@ void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, Destroy
try (DestroyableSecretKey fk = fileKey.copy()) {
// nonce:
final byte[] nonce = new byte[NONCE_SIZE];
- final ByteBuffer chunkNonceBuf = ciphertextChunk.asReadOnlyBuffer();
- chunkNonceBuf.position(0).limit(NONCE_SIZE);
- chunkNonceBuf.get(nonce);
+ ciphertextChunk.get(nonce, 0, NONCE_SIZE);
// payload:
- final ByteBuffer payloadBuf = ciphertextChunk.asReadOnlyBuffer();
+ final ByteBuffer payloadBuf = ciphertextChunk.duplicate();
payloadBuf.position(NONCE_SIZE).limit(ciphertextChunk.limit() - MAC_SIZE);
// payload:
- final Cipher cipher = CipherSupplier.AES_CTR.forDecryption(fk, new IvParameterSpec(nonce));
- assert cleartextChunk.remaining() >= cipher.getOutputSize(payloadBuf.remaining());
- cipher.doFinal(payloadBuf, cleartextChunk);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_CTR.decryptionCipher(fk, new IvParameterSpec(nonce))) {
+ assert cleartextChunk.remaining() >= cipher.get().getOutputSize(payloadBuf.remaining());
+ cipher.get().doFinal(payloadBuf, cleartextChunk);
+ }
} catch (ShortBufferException e) {
throw new IllegalStateException("Buffer allocated for reported output size apparently not big enough.", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
@@ -157,38 +158,31 @@ void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, Destroy
boolean checkChunkMac(byte[] headerNonce, long chunkNumber, ByteBuffer chunkBuf) {
assert chunkBuf.remaining() >= NONCE_SIZE + MAC_SIZE;
- // get three components: nonce + payload + mac
- final ByteBuffer chunkNonceBuf = chunkBuf.asReadOnlyBuffer();
- chunkNonceBuf.position(0).limit(NONCE_SIZE);
- final ByteBuffer payloadBuf = chunkBuf.asReadOnlyBuffer();
- payloadBuf.position(NONCE_SIZE).limit(chunkBuf.limit() - MAC_SIZE);
- final ByteBuffer expectedMacBuf = chunkBuf.asReadOnlyBuffer();
+ // get nonce + payload
+ final ByteBuffer nonceAndPayload = chunkBuf.duplicate();
+ nonceAndPayload.position(0).limit(chunkBuf.limit() - MAC_SIZE);
+ final ByteBuffer expectedMacBuf = chunkBuf.duplicate();
expectedMacBuf.position(chunkBuf.limit() - MAC_SIZE);
- // get nonce:
- final byte[] chunkNonce = new byte[NONCE_SIZE];
- chunkNonceBuf.get(chunkNonce);
-
// get expected MAC:
final byte[] expectedMac = new byte[MAC_SIZE];
expectedMacBuf.get(expectedMac);
// get actual MAC:
- final byte[] calculatedMac = calcChunkMac(headerNonce, chunkNumber, chunkNonce, payloadBuf);
+ final byte[] calculatedMac = calcChunkMac(headerNonce, chunkNumber, nonceAndPayload);
// time-constant equality check of two MACs:
return MessageDigest.isEqual(expectedMac, calculatedMac);
}
- private byte[] calcChunkMac(byte[] headerNonce, long chunkNumber, byte[] chunkNonce, ByteBuffer ciphertext) {
- try (DestroyableSecretKey mk = masterkey.getMacKey()) {
+ private byte[] calcChunkMac(byte[] headerNonce, long chunkNumber, ByteBuffer nonceAndCiphertext) {
+ try (DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease mac = MacSupplier.HMAC_SHA256.keyed(mk)) {
final byte[] chunkNumberBigEndian = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.BIG_ENDIAN).putLong(chunkNumber).array();
- final Mac mac = MacSupplier.HMAC_SHA256.withKey(mk);
- mac.update(headerNonce);
- mac.update(chunkNumberBigEndian);
- mac.update(chunkNonce);
- mac.update(ciphertext);
- return mac.doFinal();
+ mac.get().update(headerNonce);
+ mac.get().update(chunkNumberBigEndian);
+ mac.get().update(nonceAndCiphertext);
+ return mac.get().doFinal();
}
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java
index aeaa7ee..f1b0c59 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java
@@ -15,6 +15,7 @@
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MacSupplier;
+import org.cryptomator.cryptolib.common.ObjectPool;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -61,16 +62,18 @@ public ByteBuffer encryptHeader(FileHeader header) {
result.put(headerImpl.getNonce());
// encrypt payload:
- Cipher cipher = CipherSupplier.AES_CTR.forEncryption(ek, new IvParameterSpec(headerImpl.getNonce()));
- int encrypted = cipher.doFinal(payloadCleartextBuf, result);
- assert encrypted == FileHeaderImpl.Payload.SIZE;
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_CTR.encryptionCipher(ek, new IvParameterSpec(headerImpl.getNonce()))) {
+ int encrypted = cipher.get().doFinal(payloadCleartextBuf, result);
+ assert encrypted == FileHeaderImpl.Payload.SIZE;
+ }
// mac nonce and ciphertext:
ByteBuffer nonceAndCiphertextBuf = result.duplicate();
nonceAndCiphertextBuf.flip();
- Mac mac = MacSupplier.HMAC_SHA256.withKey(mk);
- mac.update(nonceAndCiphertextBuf);
- result.put(mac.doFinal());
+ try (ObjectPool.Lease mac = MacSupplier.HMAC_SHA256.keyed(mk)) {
+ mac.get().update(nonceAndCiphertextBuf);
+ result.put(mac.get().doFinal());
+ }
result.flip();
return result;
@@ -88,7 +91,7 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic
if (ciphertextHeaderBuf.remaining() < FileHeaderImpl.SIZE) {
throw new IllegalArgumentException("Malformed ciphertext header");
}
- ByteBuffer buf = ciphertextHeaderBuf.asReadOnlyBuffer();
+ ByteBuffer buf = ciphertextHeaderBuf.duplicate();
byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN];
buf.position(FileHeaderImpl.NONCE_POS);
buf.get(nonce);
@@ -100,12 +103,12 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic
buf.get(expectedMac);
// check mac:
- try (DestroyableSecretKey mk = masterkey.getMacKey()) {
+ try (DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease mac = MacSupplier.HMAC_SHA256.keyed(mk)) {
ByteBuffer nonceAndCiphertextBuf = buf.duplicate();
nonceAndCiphertextBuf.position(FileHeaderImpl.NONCE_POS).limit(FileHeaderImpl.NONCE_POS + FileHeaderImpl.NONCE_LEN + FileHeaderImpl.PAYLOAD_LEN);
- Mac mac = MacSupplier.HMAC_SHA256.withKey(mk);
- mac.update(nonceAndCiphertextBuf);
- byte[] calculatedMac = mac.doFinal();
+ mac.get().update(nonceAndCiphertextBuf);
+ byte[] calculatedMac = mac.get().doFinal();
if (!MessageDigest.isEqual(expectedMac, calculatedMac)) {
throw new AuthenticationFailedException("Header MAC doesn't match.");
}
@@ -114,10 +117,11 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic
ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE);
try (DestroyableSecretKey ek = masterkey.getEncKey()) {
// decrypt payload:
- Cipher cipher = CipherSupplier.AES_CTR.forDecryption(ek, new IvParameterSpec(nonce));
- assert cipher.getOutputSize(ciphertextPayload.length) == payloadCleartextBuf.remaining();
- int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextPayload), payloadCleartextBuf);
- assert decrypted == FileHeaderImpl.Payload.SIZE;
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_CTR.decryptionCipher(ek, new IvParameterSpec(nonce))) {
+ assert cipher.get().getOutputSize(ciphertextPayload.length) == payloadCleartextBuf.remaining();
+ int decrypted = cipher.get().doFinal(ByteBuffer.wrap(ciphertextPayload), payloadCleartextBuf);
+ assert decrypted == FileHeaderImpl.Payload.SIZE;
+ }
payloadCleartextBuf.flip();
FileHeaderImpl.Payload payload = FileHeaderImpl.Payload.decode(payloadCleartextBuf);
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java
index ad99625..5104aa5 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java
@@ -14,22 +14,19 @@
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
+import org.cryptomator.cryptolib.common.ObjectPool;
import org.cryptomator.siv.SivMode;
import org.cryptomator.siv.UnauthenticCiphertextException;
import javax.crypto.IllegalBlockSizeException;
+import java.security.MessageDigest;
import static java.nio.charset.StandardCharsets.UTF_8;
class FileNameCryptorImpl implements FileNameCryptor {
private static final BaseEncoding BASE32 = BaseEncoding.base32();
- private static final ThreadLocal AES_SIV = new ThreadLocal() {
- @Override
- protected SivMode initialValue() {
- return new SivMode();
- }
- };
+ private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new);
private final Masterkey masterkey;
@@ -39,28 +36,32 @@ protected SivMode initialValue() {
@Override
public String hashDirectoryId(String cleartextDirectoryId) {
- try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
+ try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance();
+ ObjectPool.Lease siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
- byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes);
- byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes);
+ byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
+ byte[] hashedBytes = sha1.get().digest(encryptedBytes);
return BASE32.encode(hashedBytes);
}
}
@Override
public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) {
- try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
+ try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
- byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData);
+ byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData);
return encoding.encode(encryptedBytes);
}
}
@Override
public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
- try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
+ try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease siv = AES_SIV.get()) {
byte[] encryptedBytes = encoding.decode(ciphertextName);
- byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData);
+ byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (UnauthenticCiphertextException | IllegalArgumentException | IllegalBlockSizeException e) {
throw new AuthenticationFailedException("Invalid Ciphertext.", e);
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/Constants.java b/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
index b0a7380..f6c0f4d 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
@@ -10,6 +10,9 @@
final class Constants {
+ private Constants() {
+ }
+
static final String CONTENT_ENC_ALG = "AES";
static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java
index a03eea9..affb486 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileContentCryptorImpl.java
@@ -13,6 +13,7 @@
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.ObjectPool;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
@@ -103,13 +104,14 @@ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long ch
random.nextBytes(nonce);
// payload:
- final Cipher cipher = CipherSupplier.AES_GCM.forEncryption(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
- final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
- cipher.updateAAD(chunkNumberBigEndian);
- cipher.updateAAD(headerNonce);
- ciphertextChunk.put(nonce);
- assert ciphertextChunk.remaining() >= cipher.getOutputSize(cleartextChunk.remaining());
- cipher.doFinal(cleartextChunk, ciphertextChunk);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
+ cipher.get().updateAAD(chunkNumberBigEndian);
+ cipher.get().updateAAD(headerNonce);
+ ciphertextChunk.put(nonce);
+ assert ciphertextChunk.remaining() >= cipher.get().getOutputSize(cleartextChunk.remaining());
+ cipher.get().doFinal(cleartextChunk, ciphertextChunk);
+ }
} catch (ShortBufferException e) {
throw new IllegalStateException("Buffer allocated for reported output size apparently not big enough.", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
@@ -124,22 +126,21 @@ void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long ch
try (DestroyableSecretKey fk = fileKey.copy()) {
// nonce:
final byte[] nonce = new byte[GCM_NONCE_SIZE];
- final ByteBuffer chunkNonceBuf = ciphertextChunk.asReadOnlyBuffer();
- chunkNonceBuf.position(0).limit(GCM_NONCE_SIZE);
- chunkNonceBuf.get(nonce);
+ ciphertextChunk.get(nonce, 0, GCM_NONCE_SIZE);
// payload:
- final ByteBuffer payloadBuf = ciphertextChunk.asReadOnlyBuffer();
+ final ByteBuffer payloadBuf = ciphertextChunk.duplicate();
payloadBuf.position(GCM_NONCE_SIZE);
assert payloadBuf.remaining() >= GCM_TAG_SIZE;
// payload:
- final Cipher cipher = CipherSupplier.AES_GCM.forDecryption(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
- final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
- cipher.updateAAD(chunkNumberBigEndian);
- cipher.updateAAD(headerNonce);
- assert cleartextChunk.remaining() >= cipher.getOutputSize(payloadBuf.remaining());
- cipher.doFinal(payloadBuf, cleartextChunk);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.decryptionCipher(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
+ cipher.get().updateAAD(chunkNumberBigEndian);
+ cipher.get().updateAAD(headerNonce);
+ assert cleartextChunk.remaining() >= cipher.get().getOutputSize(payloadBuf.remaining());
+ cipher.get().doFinal(payloadBuf, cleartextChunk);
+ }
} catch (AEADBadTagException e) {
throw new AuthenticationFailedException("Content tag mismatch.", e);
} catch (ShortBufferException e) {
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java
index 509a5b9..e17d85b 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java
@@ -14,6 +14,7 @@
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.ObjectPool;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
@@ -61,10 +62,10 @@ public ByteBuffer encryptHeader(FileHeader header) {
result.put(headerImpl.getNonce());
// encrypt payload:
- Cipher cipher = CipherSupplier.AES_GCM.forEncryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce()));
- int encrypted = cipher.doFinal(payloadCleartextBuf, result);
- assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN;
-
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce()))) {
+ int encrypted = cipher.get().doFinal(payloadCleartextBuf, result);
+ assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN;
+ }
result.flip();
return result;
} catch (ShortBufferException e) {
@@ -81,7 +82,7 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic
if (ciphertextHeaderBuf.remaining() < FileHeaderImpl.SIZE) {
throw new IllegalArgumentException("Malformed ciphertext header");
}
- ByteBuffer buf = ciphertextHeaderBuf.asReadOnlyBuffer();
+ ByteBuffer buf = ciphertextHeaderBuf.duplicate();
byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN];
buf.position(FileHeaderImpl.NONCE_POS);
buf.get(nonce);
@@ -93,9 +94,10 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic
ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE + GCM_TAG_SIZE);
try (DestroyableSecretKey ek = masterkey.getEncKey()) {
// decrypt payload:
- Cipher cipher = CipherSupplier.AES_GCM.forDecryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
- int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf);
- assert decrypted == FileHeaderImpl.Payload.SIZE;
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.decryptionCipher(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ int decrypted = cipher.get().doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf);
+ assert decrypted == FileHeaderImpl.Payload.SIZE;
+ }
payloadCleartextBuf.flip();
FileHeaderImpl.Payload payload = FileHeaderImpl.Payload.decode(payloadCleartextBuf);
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java
index 9f14680..0498afe 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java
@@ -14,22 +14,19 @@
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
+import org.cryptomator.cryptolib.common.ObjectPool;
import org.cryptomator.siv.SivMode;
import org.cryptomator.siv.UnauthenticCiphertextException;
import javax.crypto.IllegalBlockSizeException;
+import java.security.MessageDigest;
import static java.nio.charset.StandardCharsets.UTF_8;
class FileNameCryptorImpl implements FileNameCryptor {
private static final BaseEncoding BASE32 = BaseEncoding.base32();
- private static final ThreadLocal AES_SIV = new ThreadLocal() {
- @Override
- protected SivMode initialValue() {
- return new SivMode();
- }
- };
+ private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new);
private final Masterkey masterkey;
@@ -39,28 +36,32 @@ protected SivMode initialValue() {
@Override
public String hashDirectoryId(String cleartextDirectoryId) {
- try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
+ try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance();
+ ObjectPool.Lease siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
- byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes);
- byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes);
+ byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
+ byte[] hashedBytes = sha1.get().digest(encryptedBytes);
return BASE32.encode(hashedBytes);
}
}
@Override
public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) {
- try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
+ try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
- byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData);
+ byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData);
return encoding.encode(encryptedBytes);
}
}
@Override
public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
- try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
+ try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
+ ObjectPool.Lease siv = AES_SIV.get()) {
byte[] encryptedBytes = encoding.decode(ciphertextName);
- byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData);
+ byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) {
throw new AuthenticationFailedException("Invalid Ciphertext.", e);
diff --git a/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java
index 0b8f78f..f846629 100644
--- a/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java
@@ -8,10 +8,6 @@
*******************************************************************************/
package org.cryptomator.cryptolib.api;
-import org.cryptomator.cryptolib.api.AuthenticationFailedException;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
import org.cryptomator.cryptolib.common.SecureRandomMock;
@@ -34,12 +30,11 @@
public class CryptoLibIntegrationTest {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
- private static final Masterkey MASTERKEY = Masterkey.generate(RANDOM_MOCK);
private static Stream getCryptors() {
return Stream.of(
- CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC).provide(MASTERKEY, RANDOM_MOCK),
- CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(MASTERKEY, RANDOM_MOCK)
+ CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK),
+ CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK)
);
}
diff --git a/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java b/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java
index 03a97eb..9923cce 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java
@@ -13,10 +13,10 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
-import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.RC5ParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
+import java.security.spec.AlgorithmParameterSpec;
public class CipherSupplierTest {
@@ -30,8 +30,10 @@ public void testGetUnknownCipher() {
@Test
public void testGetCipherWithInvalidKey() {
CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding");
+ SecretKey key = new DestroyableSecretKey(new byte[13], "AES");
+ AlgorithmParameterSpec params = new IvParameterSpec(new byte[16]);
IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
- supplier.forMode(Cipher.ENCRYPT_MODE, new DestroyableSecretKey(new byte[13], "AES"), new IvParameterSpec(new byte[16]));
+ supplier.encryptionCipher(key, params);
});
MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Invalid key"));
}
@@ -39,8 +41,10 @@ public void testGetCipherWithInvalidKey() {
@Test
public void testGetCipherWithInvalidAlgorithmParam() {
CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding");
+ SecretKey key = new DestroyableSecretKey(new byte[16], "AES");
+ AlgorithmParameterSpec params = new RC5ParameterSpec(1, 1, 8);
IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
- supplier.forMode(Cipher.ENCRYPT_MODE, new DestroyableSecretKey(new byte[16], "AES"), new RC5ParameterSpec(1, 1, 8));
+ supplier.encryptionCipher(key, params);
});
MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Algorithm parameter not appropriate for"));
}
diff --git a/src/test/java/org/cryptomator/cryptolib/common/MacSupplierTest.java b/src/test/java/org/cryptomator/cryptolib/common/MacSupplierTest.java
index 9a3458d..2155138 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/MacSupplierTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/MacSupplierTest.java
@@ -15,7 +15,6 @@
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.Method;
-import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
@@ -24,27 +23,28 @@ public class MacSupplierTest {
@Test
public void testConstructorWithInvalidDigest() {
- SecretKey key = new SecretKeySpec(new byte[16], "HmacSHA256");
Assertions.assertThrows(IllegalArgumentException.class, () -> {
- new MacSupplier("FOO3000").withKey(key);
+ new MacSupplier("FOO3000");
});
}
@Test
- public void testGetMac() throws InvalidKeyException, NoSuchAlgorithmException {
+ public void testGetMac() {
SecretKey key = new SecretKeySpec(new byte[16], "HmacSHA256");
- Mac mac1 = MacSupplier.HMAC_SHA256.withKey(key);
- Assertions.assertNotNull(mac1);
+ try (ObjectPool.Lease mac1 = MacSupplier.HMAC_SHA256.keyed(key)) {
+ Assertions.assertNotNull(mac1);
+ }
- Mac mac2 = MacSupplier.HMAC_SHA256.withKey(key);
- Assertions.assertSame(mac1, mac2);
+ try (ObjectPool.Lease mac2 = MacSupplier.HMAC_SHA256.keyed(key)) {
+ Assertions.assertNotNull(mac2);
+ }
}
@Test
- public void testGetMacWithInvalidKey() throws InvalidKeyException, NoSuchAlgorithmException, ReflectiveOperationException {
+ public void testGetMacWithInvalidKey() throws NoSuchAlgorithmException, ReflectiveOperationException {
Key key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
// invoked via reflection, as we can not cast Key to SecretKey.
- Method m = MacSupplier.class.getMethod("withKey", SecretKey.class);
+ Method m = MacSupplier.class.getMethod("keyed", SecretKey.class);
Assertions.assertThrows(IllegalArgumentException.class, () -> {
m.invoke(MacSupplier.HMAC_SHA256, key);
});
diff --git a/src/test/java/org/cryptomator/cryptolib/common/MessageDigestSupplierTest.java b/src/test/java/org/cryptomator/cryptolib/common/MessageDigestSupplierTest.java
index ad67c66..6c4d290 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/MessageDigestSupplierTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/MessageDigestSupplierTest.java
@@ -18,17 +18,19 @@ public class MessageDigestSupplierTest {
@Test
public void testConstructorWithInvalidDigest() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
- new MessageDigestSupplier("FOO3000").get();
+ new MessageDigestSupplier("FOO3000");
});
}
@Test
public void testGetSha1() {
- MessageDigest digest1 = MessageDigestSupplier.SHA1.get();
- Assertions.assertNotNull(digest1);
+ try (ObjectPool.Lease digest = MessageDigestSupplier.SHA1.instance()) {
+ Assertions.assertNotNull(digest);
+ }
- MessageDigest digest2 = MessageDigestSupplier.SHA1.get();
- Assertions.assertSame(digest1, digest2);
+ try (ObjectPool.Lease digest = MessageDigestSupplier.SHA1.instance()) {
+ Assertions.assertNotNull(digest);
+ }
}
}
diff --git a/src/test/java/org/cryptomator/cryptolib/common/ObjectPoolTest.java b/src/test/java/org/cryptomator/cryptolib/common/ObjectPoolTest.java
new file mode 100644
index 0000000..a435c7d
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/common/ObjectPoolTest.java
@@ -0,0 +1,61 @@
+package org.cryptomator.cryptolib.common;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.function.Supplier;
+
+public class ObjectPoolTest {
+
+ private Supplier factory = Mockito.mock(Supplier.class);
+ private ObjectPool pool = new ObjectPool<>(factory);
+
+ @BeforeEach
+ public void setup() {
+ Mockito.doAnswer(invocation -> new Foo()).when(factory).get();
+ }
+
+ @Test
+ @DisplayName("new instance is created if pool is empty")
+ public void testCreateNewObjWhenPoolIsEmpty() {
+ try (ObjectPool.Lease lease1 = pool.get()) {
+ try (ObjectPool.Lease lease2 = pool.get()) {
+ Assertions.assertNotSame(lease1.get(), lease2.get());
+ }
+ }
+ Mockito.verify(factory, Mockito.times(2)).get();
+ }
+
+ @Test
+ @DisplayName("recycle existing instance")
+ public void testRecycleExistingObj() {
+ Foo foo1;
+ try (ObjectPool.Lease lease = pool.get()) {
+ foo1 = lease.get();
+ }
+ try (ObjectPool.Lease lease = pool.get()) {
+ Assertions.assertSame(foo1, lease.get());
+ }
+ Mockito.verify(factory, Mockito.times(1)).get();
+ }
+
+ @Test
+ @DisplayName("create new instance when pool is GC'ed")
+ public void testGc() {
+ try (ObjectPool.Lease lease = pool.get()) {
+ Assertions.assertNotNull(lease.get());
+ }
+ System.gc(); // seems to be reliable on Temurin 17 with @RepeatedTest(1000)
+ try (ObjectPool.Lease lease = pool.get()) {
+ Assertions.assertNotNull(lease.get());
+ }
+ Mockito.verify(factory, Mockito.times(2)).get();
+ }
+
+ private static class Foo {
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/common/PooledSuppliersBenchmarkTest.java b/src/test/java/org/cryptomator/cryptolib/common/PooledSuppliersBenchmarkTest.java
new file mode 100644
index 0000000..471515a
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/common/PooledSuppliersBenchmarkTest.java
@@ -0,0 +1,99 @@
+package org.cryptomator.cryptolib.common;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.MessageDigest;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+@State(Scope.Thread)
+@Warmup(iterations = 2)
+@Measurement(iterations = 2)
+@BenchmarkMode(value = {Mode.AverageTime})
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class PooledSuppliersBenchmarkTest {
+
+ private static final Random RNG = new Random(42);
+ private static final CipherSupplier CIPHER_SUPPLIER = CipherSupplier.AES_GCM;
+ private static final MessageDigestSupplier MD_SUPPLIER = MessageDigestSupplier.SHA256;
+ private static final MacSupplier MAC_SUPPLIER = MacSupplier.HMAC_SHA256;
+ private SecretKey key;
+ private GCMParameterSpec gcmParams;
+
+ @Disabled("only on demand")
+ @Test
+ public void runBenchmarks() throws RunnerException {
+ Options opt = new OptionsBuilder() //
+ .include(getClass().getName()) //
+ .threads(2).forks(1) //
+ .shouldFailOnError(true).shouldDoGC(true) //
+ .build();
+ new Runner(opt).run();
+ }
+
+ @Setup(Level.Invocation)
+ public void shuffleData() {
+ byte[] bytes = new byte[28];
+ RNG.nextBytes(bytes);
+ this.key = new SecretKeySpec(bytes, 0, 16, "AES");
+ this.gcmParams = new GCMParameterSpec(128, bytes, 16, 12);
+ }
+
+ @Benchmark
+ public void createCipher(Blackhole blackHole) {
+ blackHole.consume(CIPHER_SUPPLIER.forEncryption(key, gcmParams));
+ }
+
+ @Benchmark
+ public void recycleCipher(Blackhole blackHole) {
+ try (ObjectPool.Lease lease = CIPHER_SUPPLIER.encryptionCipher(key, gcmParams)) {
+ blackHole.consume(lease.get());
+ }
+ }
+
+ @Benchmark
+ public void createMac(Blackhole blackHole) {
+ blackHole.consume(MAC_SUPPLIER.withKey(key));
+ }
+
+ @Benchmark
+ public void recycleMac(Blackhole blackHole) {
+ try (ObjectPool.Lease lease = MAC_SUPPLIER.keyed(key)) {
+ blackHole.consume(lease.get());
+ }
+ }
+
+ @Benchmark
+ public void createMd(Blackhole blackHole) {
+ blackHole.consume(MD_SUPPLIER.get());
+ }
+
+ @Benchmark
+ public void recycleMd(Blackhole blackHole) {
+ try (ObjectPool.Lease lease = MD_SUPPLIER.instance()) {
+ blackHole.consume(lease.get());
+ }
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/common/SeekableByteChannelMock.java b/src/test/java/org/cryptomator/cryptolib/common/SeekableByteChannelMock.java
index 9f2db2f..8aa9116 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/SeekableByteChannelMock.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/SeekableByteChannelMock.java
@@ -27,7 +27,7 @@ public boolean isOpen() {
}
@Override
- public void close() throws IOException {
+ public void close() {
open = false;
}
@@ -46,7 +46,7 @@ public int read(ByteBuffer dst) throws IOException {
}
@Override
- public int write(ByteBuffer src) throws IOException {
+ public int write(ByteBuffer src) {
int num = Math.min(buf.remaining(), src.remaining());
ByteBuffer limitedSrc = src.asReadOnlyBuffer();
limitedSrc.limit(limitedSrc.position() + num);
@@ -55,12 +55,12 @@ public int write(ByteBuffer src) throws IOException {
}
@Override
- public long position() throws IOException {
+ public long position() {
return buf.position();
}
@Override
- public SeekableByteChannel position(long newPosition) throws IOException {
+ public SeekableByteChannel position(long newPosition) {
assert newPosition < Integer.MAX_VALUE;
buf.position((int) newPosition);
return this;
@@ -72,7 +72,7 @@ public long size() throws IOException {
}
@Override
- public SeekableByteChannel truncate(long size) throws IOException {
+ public SeekableByteChannel truncate(long size) {
assert size < Integer.MAX_VALUE;
if (size < buf.position()) {
buf.position((int) size);
diff --git a/src/test/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonceTest.java b/src/test/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonceTest.java
index 91ed77a..48cb923 100644
--- a/src/test/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonceTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonceTest.java
@@ -2,6 +2,7 @@
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.GcmTestHelper;
+import org.cryptomator.cryptolib.common.ObjectPool;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -10,6 +11,7 @@
import org.junit.jupiter.params.provider.CsvSource;
import javax.crypto.AEADBadTagException;
+import javax.crypto.Cipher;
/**
* Test vectors from https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf
@@ -22,7 +24,9 @@ public class GcmWithSecretNonceTest {
public void setup() {
// reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse
GcmTestHelper.reset((mode, key, params) -> {
- CipherSupplier.AES_GCM.forEncryption(key, params);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, params)) {
+ cipher.get();
+ }
});
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java
index 007fe3f..ea6fc9a 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java
@@ -38,7 +38,6 @@
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
-import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java
index 11fa682..5605700 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java
@@ -69,28 +69,28 @@ public void testDecryption() throws AuthenticationFailedException {
@Test
public void testDecryptionWithTooShortHeader() {
- byte[] ciphertext = new byte[7];
+ ByteBuffer ciphertext = ByteBuffer.allocate(7);
Assertions.assertThrows(IllegalArgumentException.class, () -> {
- headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
+ headerCryptor.decryptHeader(ciphertext);
});
}
@Test
public void testDecryptionWithInvalidMac1() {
- byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa==");
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa=="));
Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
+ headerCryptor.decryptHeader(ciphertext);
});
}
@Test
public void testDecryptionWithInvalidMac2() {
- byte[] ciphertext = BaseEncoding.base64().decode("aAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA==");
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("aAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
+ headerCryptor.decryptHeader(ciphertext);
});
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java
index ec77ab0..99ef409 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java
@@ -97,9 +97,10 @@ public void testDecryptionOfMalformedFilename() {
public void testDecryptionOfManipulatedFilename() {
final byte[] encrypted = filenameCryptor.encryptFilename(BASE32, "test").getBytes(UTF_8);
encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte
+ String ciphertextName = new String(encrypted, UTF_8);
AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- filenameCryptor.decryptFilename(BASE32, new String(encrypted, UTF_8));
+ filenameCryptor.decryptFilename(BASE32, ciphertextName);
});
MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class));
}
@@ -124,9 +125,10 @@ public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws Au
@DisplayName("decrypt ciphertext with incorrect AD")
public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() {
final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "right".getBytes(UTF_8));
+ final byte[] ad = "wrong".getBytes(UTF_8);
Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- filenameCryptor.decryptFilename(BASE32, encrypted, "wrong".getBytes(UTF_8));
+ filenameCryptor.decryptFilename(BASE32, encrypted, ad);
});
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java
index 1cfc50f..baea1a5 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java
@@ -14,11 +14,13 @@
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.GcmTestHelper;
+import org.cryptomator.cryptolib.common.ObjectPool;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import javax.crypto.Cipher;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
@@ -35,7 +37,9 @@ public void setup() {
// reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse
GcmTestHelper.reset((mode, key, params) -> {
- CipherSupplier.AES_GCM.forEncryption(key, params);
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, params)) {
+ cipher.get();
+ }
});
}
@@ -74,25 +78,28 @@ public void testDecryption() throws AuthenticationFailedException {
@Test
public void testDecryptionWithTooShortHeader() {
- byte[] ciphertext = new byte[7];
+ ByteBuffer ciphertext = ByteBuffer.allocate(7);
+
Assertions.assertThrows(IllegalArgumentException.class, () -> {
- headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
+ headerCryptor.decryptHeader(ciphertext);
});
}
@Test
public void testDecryptionWithInvalidTag1() {
- byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUA=");
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUA="));
+
Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
+ headerCryptor.decryptHeader(ciphertext);
});
}
@Test
public void testDecryptionWithInvalidTag2() {
- byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUa=");
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUa="));
+
Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
+ headerCryptor.decryptHeader(ciphertext);
});
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java
index daa5da1..d808f89 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java
@@ -98,9 +98,10 @@ public void testDecryptionOfMalformedFilename() {
public void testDecryptionOfManipulatedFilename() {
final byte[] encrypted = filenameCryptor.encryptFilename(BASE32, "test").getBytes(UTF_8);
encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte
+ String ciphertextName = new String(encrypted, UTF_8);
AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- filenameCryptor.decryptFilename(BASE32, new String(encrypted, UTF_8));
+ filenameCryptor.decryptFilename(BASE32, ciphertextName);
});
MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class));
}
@@ -125,9 +126,10 @@ public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws Au
@DisplayName("decrypt ciphertext with incorrect AD")
public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() {
final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "right".getBytes(UTF_8));
+ final byte[] ad = "wrong".getBytes(UTF_8);
Assertions.assertThrows(AuthenticationFailedException.class, () -> {
- filenameCryptor.decryptFilename(BASE32, encrypted, "wrong".getBytes(UTF_8));
+ filenameCryptor.decryptFilename(BASE32, encrypted, ad);
});
}