diff --git a/pom.xml b/pom.xml index c694277..8020be3 100644 --- a/pom.xml +++ b/pom.xml @@ -19,19 +19,19 @@ 2.8.9 - 30.1.1-jre - 1.4.3 - 1.69 - 1.7.31 + 31.0.1-jre + 1.4.4 + 1.70 + 1.7.35 - 5.7.2 - 3.11.2 + 5.8.2 + 4.3.1 2.2 - 1.32 + 1.34 - 6.2.2 + 6.5.3 0.8.7 1.6.8 @@ -131,7 +131,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0-M3 + 3.0.0 enforce-java @@ -151,7 +151,7 @@ maven-compiler-plugin - 3.8.1 + 3.9.0 UTF-8 true @@ -197,7 +197,7 @@ org.moditect moditect-maven-plugin - 1.0.0.RC1 + 1.0.0.RC2 add-module-infos @@ -213,7 +213,7 @@ module org.cryptomator.cryptolib { requires org.cryptomator.siv; requires com.google.gson; - requires com.google.common; + requires transitive com.google.common; requires org.slf4j; exports org.cryptomator.cryptolib.api; @@ -238,12 +238,12 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.0.0-M5 org.apache.maven.plugins maven-jar-plugin - 3.2.0 + 3.2.2 @@ -267,7 +267,7 @@ maven-javadoc-plugin - 3.3.0 + 3.3.1 attach-javadocs diff --git a/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java b/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java index 0e9bcd9..86e7489 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java +++ b/src/main/java/org/cryptomator/cryptolib/common/AesKeyWrap.java @@ -16,23 +16,26 @@ public class AesKeyWrap { + private AesKeyWrap() { + } + /** * @param kek Key encrypting key * @param key Key to be wrapped * @return Wrapped key */ public static byte[] wrap(DestroyableSecretKey kek, SecretKey key) { - try (DestroyableSecretKey kekCopy = kek.copy()) { - final Cipher cipher = CipherSupplier.RFC3394_KEYWRAP.forWrapping(kekCopy); - return cipher.wrap(key); + try (DestroyableSecretKey kekCopy = kek.copy(); + ObjectPool.Lease cipher = CipherSupplier.RFC3394_KEYWRAP.keyWrapCipher(kekCopy)) { + return cipher.get().wrap(key); } catch (InvalidKeyException | IllegalBlockSizeException e) { throw new IllegalArgumentException("Unable to wrap key.", e); } } /** - * @param kek Key encrypting key - * @param wrappedKey Key to be unwrapped + * @param kek Key encrypting key + * @param wrappedKey Key to be unwrapped * @param wrappedKeyAlgorithm Key designation, i.e. algorithm to be associated with the unwrapped key. * @return Unwrapped key * @throws InvalidKeyException If unwrapping failed (i.e. wrong kek) @@ -43,9 +46,9 @@ public static DestroyableSecretKey unwrap(DestroyableSecretKey kek, byte[] wrapp // visible for testing static DestroyableSecretKey unwrap(DestroyableSecretKey kek, byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) throws InvalidKeyException { - try (DestroyableSecretKey kekCopy = kek.copy()) { - final Cipher cipher = CipherSupplier.RFC3394_KEYWRAP.forUnwrapping(kekCopy); - return DestroyableSecretKey.from(cipher.unwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType)); + try (DestroyableSecretKey kekCopy = kek.copy(); + ObjectPool.Lease cipher = CipherSupplier.RFC3394_KEYWRAP.keyUnwrapCipher(kekCopy)) { + return DestroyableSecretKey.from(cipher.get().unwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType)); } catch (NoSuchAlgorithmException e) { throw new IllegalArgumentException("Invalid algorithm: " + wrappedKeyAlgorithm, e); } diff --git a/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java b/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java index d30ee36..d80662e 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java +++ b/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java @@ -14,6 +14,9 @@ public class ByteBuffers { + private ByteBuffers() { + } + /** * Copies as many bytes as possible from the given source to the destination buffer. * The position of both buffers will be incremented by as many bytes as have been copied. diff --git a/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java b/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java index edbc13e..36c3b22 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java +++ b/src/main/java/org/cryptomator/cryptolib/common/CipherSupplier.java @@ -15,7 +15,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; -import java.util.function.Function; public final class CipherSupplier { @@ -24,51 +23,140 @@ public final class CipherSupplier { public static final CipherSupplier RFC3394_KEYWRAP = new CipherSupplier("AESWrap"); private final String cipherAlgorithm; - private final ThreadLocal threadLocal; + private final ObjectPool cipherPool; public CipherSupplier(String cipherAlgorithm) { this.cipherAlgorithm = cipherAlgorithm; - this.threadLocal = new Provider(); - this.threadLocal.get(); // eagerly initialize to provoke exceptions + this.cipherPool = new ObjectPool<>(this::createCipher); + try (ObjectPool.Lease lease = cipherPool.get()) { + lease.get(); // eagerly initialize to provoke exceptions + } } - private class Provider extends ThreadLocal { - @Override - protected Cipher initialValue() { - try { - return Cipher.getInstance(cipherAlgorithm); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new IllegalArgumentException("Invalid cipher algorithm or padding.", e); - } + private Cipher createCipher() { + try { + return Cipher.getInstance(cipherAlgorithm); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("Invalid cipher algorithm or padding.", e); } } + /** + * Leases a reusable cipher object initialized for encryption. + * + * @param key Encryption key + * @param params Params such as IV/Nonce + * @return A lease supplying a refurbished Cipher + */ + public ObjectPool.Lease encryptionCipher(SecretKey key, AlgorithmParameterSpec params) { + ObjectPool.Lease lease = cipherPool.get(); + initMode(lease.get(), Cipher.ENCRYPT_MODE, key, params); + return lease; + } + + /** + * Creates a new Cipher object initialized for encryption. + * + * @param key Encryption key + * @param params Params such as IV/Nonce + * @return New Cipher instance + * @deprecated Use {@link #encryptionCipher(SecretKey, AlgorithmParameterSpec)} instead. + */ + @Deprecated public Cipher forEncryption(SecretKey key, AlgorithmParameterSpec params) { - return forMode(Cipher.ENCRYPT_MODE, key, params); + final Cipher cipher = createCipher(); + initMode(cipher, Cipher.ENCRYPT_MODE, key, params); + return cipher; + } + + /** + * Leases a reusable cipher object initialized for decryption. + * + * @param key Decryption key + * @param params Params such as IV/Nonce + * @return A lease supplying a refurbished Cipher + */ + public ObjectPool.Lease decryptionCipher(SecretKey key, AlgorithmParameterSpec params) { + ObjectPool.Lease lease = cipherPool.get(); + initMode(lease.get(), Cipher.DECRYPT_MODE, key, params); + return lease; } + /** + * Creates a new Cipher object initialized for decryption. + * + * @param key Encryption key + * @param params Params such as IV/Nonce + * @return New Cipher instance + * @deprecated Use {@link #decryptionCipher(SecretKey, AlgorithmParameterSpec)} instead. + */ + @Deprecated public Cipher forDecryption(SecretKey key, AlgorithmParameterSpec params) { - return forMode(Cipher.DECRYPT_MODE, key, params); + final Cipher cipher = createCipher(); + initMode(cipher, Cipher.DECRYPT_MODE, key, params); + return cipher; } + /** + * Leases a reusable cipher object initialized for wrapping a key. + * + * @param kek Key encryption key + * @return A lease supplying a refurbished Cipher + */ + public ObjectPool.Lease keyWrapCipher(SecretKey kek) { + ObjectPool.Lease lease = cipherPool.get(); + initMode(lease.get(), Cipher.WRAP_MODE, kek, null); + return lease; + } + + /** + * Creates a new Cipher object initialized for wrapping a key. + * + * @param kek Key encryption key + * @return New Cipher instance + * @deprecated Use {@link #keyWrapCipher(SecretKey)} instead. + */ + @Deprecated public Cipher forWrapping(SecretKey kek) { - return forMode(Cipher.WRAP_MODE, kek, null); + final Cipher cipher = createCipher(); + initMode(cipher, Cipher.WRAP_MODE, kek, null); + return cipher; } + /** + * Leases a reusable cipher object initialized for unwrapping a key. + * + * @param kek Key encryption key + * @return A lease supplying a refurbished Cipher + */ + public ObjectPool.Lease keyUnwrapCipher(SecretKey kek) { + ObjectPool.Lease lease = cipherPool.get(); + initMode(lease.get(), Cipher.UNWRAP_MODE, kek, null); + return lease; + } + + /** + * Creates a new Cipher object initialized for unwrapping a key. + * + * @param kek Key encryption key + * @return New Cipher instance + * @deprecated Use {@link #keyUnwrapCipher(SecretKey)} instead. + */ + @Deprecated public Cipher forUnwrapping(SecretKey kek) { - return forMode(Cipher.UNWRAP_MODE, kek, null); + final Cipher cipher = createCipher(); + initMode(cipher, Cipher.UNWRAP_MODE, kek, null); + return cipher; } - // visible for testing - Cipher forMode(int ciphermode, SecretKey key, AlgorithmParameterSpec params) { - final Cipher cipher = threadLocal.get(); + private void initMode(Cipher cipher, int ciphermode, SecretKey key, AlgorithmParameterSpec params) { try { cipher.init(ciphermode, key, params); - return cipher; } catch (InvalidKeyException e) { throw new IllegalArgumentException("Invalid key.", e); } catch (InvalidAlgorithmParameterException e) { throw new IllegalArgumentException("Algorithm parameter not appropriate for " + cipher.getAlgorithm() + ".", e); } } + } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java index 4f91ea2..f28baeb 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java +++ b/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java @@ -29,6 +29,8 @@ */ public class DestroyableSecretKey implements SecretKey, AutoCloseable { + private static final String KEY_DESTROYED_ERROR = "Key has been destroyed"; + private final transient byte[] key; private final String algorithm; private boolean destroyed; @@ -95,13 +97,13 @@ public static DestroyableSecretKey generate(SecureRandom csprng, String algorith @Override public String getAlgorithm() { - Preconditions.checkState(!destroyed, "Key has been destroyed"); + Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); return algorithm; } @Override public String getFormat() { - Preconditions.checkState(!destroyed, "Key has been destroyed"); + Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); return "RAW"; } @@ -115,7 +117,7 @@ public String getFormat() { */ @Override public byte[] getEncoded() { - Preconditions.checkState(!destroyed, "Key has been destroyed"); + Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); return key; } @@ -124,7 +126,7 @@ public byte[] getEncoded() { * @return New copy of this */ public DestroyableSecretKey copy() { - Preconditions.checkState(!destroyed, "Key has been destroyed"); + Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR); return new DestroyableSecretKey(key, algorithm); // key will get copied by the constructor as per contract } diff --git a/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java b/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java index 163a00f..84fe04d 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java +++ b/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java @@ -5,6 +5,9 @@ public class Destroyables { + private Destroyables() { + } + public static void destroySilently(Destroyable destroyable) { if (destroyable == null) { return; diff --git a/src/main/java/org/cryptomator/cryptolib/common/MacSupplier.java b/src/main/java/org/cryptomator/cryptolib/common/MacSupplier.java index f58514b..6093489 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MacSupplier.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MacSupplier.java @@ -8,40 +8,63 @@ *******************************************************************************/ package org.cryptomator.cryptolib.common; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - import javax.crypto.Mac; import javax.crypto.SecretKey; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; public final class MacSupplier { public static final MacSupplier HMAC_SHA256 = new MacSupplier("HmacSHA256"); private final String macAlgorithm; - private final ThreadLocal threadLocal; + private final ObjectPool macPool; public MacSupplier(String macAlgorithm) { this.macAlgorithm = macAlgorithm; - this.threadLocal = new Provider(); + this.macPool = new ObjectPool<>(this::createMac); + try (ObjectPool.Lease lease = macPool.get()) { + lease.get(); // eagerly initialize to provoke exceptions + } } - private class Provider extends ThreadLocal { - @Override - protected Mac initialValue() { - try { - return Mac.getInstance(macAlgorithm); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Invalid MAC algorithm.", e); - } + private Mac createMac() { + try { + return Mac.getInstance(macAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Invalid MAC algorithm.", e); } } + /** + * Leases a reusable MAC object initialized with the given key. + * + * @param key Key to use in keyed MAC + * @return A lease supplying a refurbished MAC + */ + public ObjectPool.Lease keyed(SecretKey key) { + ObjectPool.Lease lease = macPool.get(); + init(lease.get(), key); + return lease; + } + + /** + * Creates a new MAC + * + * @param key Key to use in keyed MAC + * @return New Mac instance + * @deprecated Use {@link #keyed(SecretKey)} instead + */ + @Deprecated public Mac withKey(SecretKey key) { + final Mac mac = createMac(); + init(mac, key); + return mac; + } + + private void init(Mac mac, SecretKey key) { try { - final Mac mac = threadLocal.get(); mac.init(key); - return mac; } catch (InvalidKeyException e) { throw new IllegalArgumentException("Invalid key.", e); } diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java index 45e3e92..dc43d97 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java @@ -192,9 +192,10 @@ MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersio final byte[] salt = new byte[DEFAULT_SCRYPT_SALT_LENGTH]; csprng.nextBytes(salt); - try (DestroyableSecretKey kek = scrypt(passphrase, salt, pepper, scryptCostParam, DEFAULT_SCRYPT_BLOCK_SIZE)) { - final Mac mac = MacSupplier.HMAC_SHA256.withKey(masterkey.getMacKey()); - final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); + try (DestroyableSecretKey kek = scrypt(passphrase, salt, pepper, scryptCostParam, DEFAULT_SCRYPT_BLOCK_SIZE); + DestroyableSecretKey macKey = masterkey.getMacKey(); + ObjectPool.Lease mac = MacSupplier.HMAC_SHA256.keyed(macKey)) { + final byte[] versionMac = mac.get().doFinal(ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(vaultVersion).array()); MasterkeyFile result = new MasterkeyFile(); result.version = vaultVersion; result.versionMac = versionMac; diff --git a/src/main/java/org/cryptomator/cryptolib/common/MessageDigestSupplier.java b/src/main/java/org/cryptomator/cryptolib/common/MessageDigestSupplier.java index 12c3ca7..5be3de5 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/MessageDigestSupplier.java +++ b/src/main/java/org/cryptomator/cryptolib/common/MessageDigestSupplier.java @@ -17,26 +17,44 @@ public final class MessageDigestSupplier { public static final MessageDigestSupplier SHA256 = new MessageDigestSupplier("SHA-256"); private final String digestAlgorithm; - private final ThreadLocal threadLocal; + private final ObjectPool mdPool; public MessageDigestSupplier(String digestAlgorithm) { this.digestAlgorithm = digestAlgorithm; - this.threadLocal = new Provider(); + this.mdPool = new ObjectPool<>(this::createMessageDigest); + try (ObjectPool.Lease lease = mdPool.get()) { + lease.get(); // eagerly initialize to provoke exceptions + } } - private class Provider extends ThreadLocal { - @Override - protected MessageDigest initialValue() { - try { - return MessageDigest.getInstance(digestAlgorithm); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Invalid digest algorithm.", e); - } + private MessageDigest createMessageDigest() { + try { + return MessageDigest.getInstance(digestAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Invalid digest algorithm.", e); } } + /** + * Leases a reusable MessageDigest. + * + * @return A ReusableMessageDigest instance holding a refurbished MessageDigest + */ + public ObjectPool.Lease instance() { + ObjectPool.Lease 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); }); }