diff --git a/pom.xml b/pom.xml index 1cf854f..2189bb5 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptolib - 2.0.3 + 2.1.0 Cryptomator Crypto Library This library contains all cryptographic functions that are used by Cryptomator. https://github.com/cryptomator/cryptolib @@ -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,15 +151,31 @@ maven-compiler-plugin - 3.8.1 + 3.9.0 UTF-8 true + + + java9 + compile + + compile + + + 9 + + ${project.basedir}/src/main/java9 + + true + + + maven-shade-plugin - 3.2.4 + 3.4.0 package @@ -179,7 +195,7 @@ org.bouncycastle - org.cryptomator.cryptolib.org.bouncycastle + org.cryptomator.cryptolib.shaded.bouncycastle @@ -194,56 +210,15 @@ - - org.moditect - moditect-maven-plugin - 1.0.0.RC1 - - - add-module-infos - package - - add-module-info - - - 9 - true - - - module org.cryptomator.cryptolib { - requires org.cryptomator.siv; - requires com.google.gson; - requires com.google.common; - requires org.slf4j; - - exports org.cryptomator.cryptolib.api; - exports org.cryptomator.cryptolib.common; - - opens org.cryptomator.cryptolib.common to com.google.gson; - - uses org.cryptomator.cryptolib.api.CryptorProvider; - - provides org.cryptomator.cryptolib.api.CryptorProvider - with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl; - } - - - - --multi-release=9 - - - - - 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 +242,7 @@ maven-javadoc-plugin - 3.3.0 + 3.3.1 attach-javadocs @@ -277,6 +252,8 @@ + 9 + ${project.basedir}/src/main/java9:${project.build.sourceDirectory} 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/ECKeyPair.java b/src/main/java/org/cryptomator/cryptolib/common/ECKeyPair.java index 8092e05..6bf04a7 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/ECKeyPair.java +++ b/src/main/java/org/cryptomator/cryptolib/common/ECKeyPair.java @@ -3,22 +3,30 @@ import com.google.common.base.Preconditions; import javax.security.auth.Destroyable; +import java.math.BigInteger; import java.security.KeyPair; import java.security.MessageDigest; +import java.security.PublicKey; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.EllipticCurve; import java.util.Arrays; import java.util.Objects; public class ECKeyPair implements Destroyable { + private static final String INVALID_KEY_ERROR = "Invalid EC Key"; + private final KeyPair keyPair; private boolean destroyed; - ECKeyPair(KeyPair keyPair) { + ECKeyPair(KeyPair keyPair, ECParameterSpec curveParams) { Preconditions.checkArgument(keyPair.getPrivate() instanceof ECPrivateKey); Preconditions.checkArgument(keyPair.getPublic() instanceof ECPublicKey); - this.keyPair = keyPair; + this.keyPair = verify(keyPair, curveParams); } public KeyPair keyPair() { @@ -37,6 +45,35 @@ public ECPublicKey getPublic() { return (ECPublicKey) keyPair.getPublic(); } + // validations taken from https://neilmadden.blog/2017/05/17/so-how-do-you-validate-nist-ecdh-public-keys/ + private static KeyPair verify(KeyPair keyPair, ECParameterSpec curveParams) { + PublicKey pk = keyPair.getPublic(); + Preconditions.checkArgument(pk instanceof ECPublicKey, INVALID_KEY_ERROR); + Preconditions.checkArgument(curveParams.getCofactor() == 1, "Verifying points on curves with cofactor not supported"); // see "Step 4" in linked post + ECPublicKey publicKey = (ECPublicKey) pk; + EllipticCurve curve = curveParams.getCurve(); + + // Step 1: Verify public key is not point at infinity. + Preconditions.checkArgument(!ECPoint.POINT_INFINITY.equals(publicKey.getW()), INVALID_KEY_ERROR); + + final BigInteger x = publicKey.getW().getAffineX(); + final BigInteger y = publicKey.getW().getAffineY(); + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); + + // Step 2: Verify x and y are in range [0,p-1] + Preconditions.checkArgument(x.compareTo(BigInteger.ZERO) >= 0 && x.compareTo(p) < 0, INVALID_KEY_ERROR); + Preconditions.checkArgument(y.compareTo(BigInteger.ZERO) >= 0 && y.compareTo(p) < 0, INVALID_KEY_ERROR); + + // Step 3: Verify that y^2 == x^3 + ax + b (mod p) + final BigInteger a = curve.getA(); + final BigInteger b = curve.getB(); + final BigInteger ySquared = y.modPow(BigInteger.valueOf(2), p); + final BigInteger xCubedPlusAXPlusB = x.modPow(BigInteger.valueOf(3), p).add(a.multiply(x)).add(b).mod(p); + Preconditions.checkArgument(ySquared.equals(xCubedPlusAXPlusB), INVALID_KEY_ERROR); + + return keyPair; + } + @Override public boolean isDestroyed() { return destroyed; 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/MasterkeyHubAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyHubAccess.java deleted file mode 100644 index be0fef0..0000000 --- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyHubAccess.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.cryptomator.cryptolib.common; - -import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -import org.cryptomator.cryptolib.ecies.EncryptedMessage; -import org.cryptomator.cryptolib.ecies.ECIntegratedEncryptionScheme; - -import javax.crypto.AEADBadTagException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Arrays; - -public class MasterkeyHubAccess { - - private static final BaseEncoding BASE64_URL = BaseEncoding.base64Url().omitPadding(); - - private MasterkeyHubAccess() { - } - - /** - * Decrypts a masterkey retrieved from Cryptomator Hub - * - * @param devicePrivateKey Private key of the device this ciphertext is intended for - * @param encodedCiphertext The encrypted masterkey - * @param encodedEphPubKey The ephemeral public key to be used to derive a secret shared between message sender and this device - * @return The decrypted masterkey - * @throws MasterkeyLoadingFailedException If the parameters don't match and decryption fails - */ - public static Masterkey decryptMasterkey(ECPrivateKey devicePrivateKey, String encodedCiphertext, String encodedEphPubKey) throws MasterkeyLoadingFailedException { - byte[] cleartext = new byte[0]; - try { - EncryptedMessage message = decode(encodedCiphertext, encodedEphPubKey); - cleartext = ECIntegratedEncryptionScheme.HUB.decrypt(devicePrivateKey, message); - return new Masterkey(cleartext); - } catch (IllegalArgumentException | AEADBadTagException e) { - throw new MasterkeyLoadingFailedException("Key and ciphertext don't match", e); - } finally { - Arrays.fill(cleartext, (byte) 0x00); - } - } - - private static EncryptedMessage decode(String encodedCiphertext, String encodedEphPubKey) throws IllegalArgumentException { - byte[] ciphertext = BASE64_URL.decode(encodedCiphertext); - byte[] keyBytes = BASE64_URL.decode(encodedEphPubKey); - try { - PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes)); - if (key instanceof ECPublicKey) { - return new EncryptedMessage((ECPublicKey) key, ciphertext); - } else { - throw new IllegalArgumentException("Key not an EC public key."); - } - } catch (InvalidKeySpecException e) { - throw new IllegalArgumentException("Invalid license public key", e); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } - -} 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/P384KeyPair.java b/src/main/java/org/cryptomator/cryptolib/common/P384KeyPair.java index a6ecbbb..feb7d5f 100644 --- a/src/main/java/org/cryptomator/cryptolib/common/P384KeyPair.java +++ b/src/main/java/org/cryptomator/cryptolib/common/P384KeyPair.java @@ -7,11 +7,20 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.security.AlgorithmParameters; import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; public class P384KeyPair extends ECKeyPair { @@ -20,7 +29,7 @@ public class P384KeyPair extends ECKeyPair { private static final String SIGNATURE_ALG = "SHA384withECDSA"; private P384KeyPair(KeyPair keyPair) { - super(keyPair); + super(keyPair, getCurveParams()); } public static P384KeyPair generate() { @@ -28,6 +37,25 @@ public static P384KeyPair generate() { return new P384KeyPair(keyPair); } + /** + * Creates a key pair from the given key specs. + * + * @param publicKeySpec DER formatted public key + * @param privateKeySpec DER formatted private key + * @return created key pair + * @throws InvalidKeySpecException If the supplied key specs are unsuitable for {@value #EC_ALG} keys + */ + public static P384KeyPair create(X509EncodedKeySpec publicKeySpec, PKCS8EncodedKeySpec privateKeySpec) throws InvalidKeySpecException { + try { + KeyFactory factory = KeyFactory.getInstance(EC_ALG); + PublicKey publicKey = factory.generatePublic(publicKeySpec); + PrivateKey privateKey = factory.generatePrivate(privateKeySpec); + return new P384KeyPair(new KeyPair(publicKey, privateKey)); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(EC_ALG + " not supported"); + } + } + /** * Loads a key pair from the given file * @@ -97,4 +125,16 @@ private static KeyPairGenerator getKeyPairGenerator() { } } + private static ECParameterSpec getCurveParams() { + try { + AlgorithmParameters parameters = AlgorithmParameters.getInstance(EC_ALG); + parameters.init(new ECGenParameterSpec(EC_CURVE_NAME)); + return parameters.getParameterSpec(ECParameterSpec.class); + } catch (NoSuchAlgorithmException | InvalidParameterSpecException e) { + throw new IllegalStateException(EC_CURVE_NAME + " curve not supported"); + } + } + + + } 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/AuthenticatedEncryption.java b/src/main/java/org/cryptomator/cryptolib/ecies/AuthenticatedEncryption.java deleted file mode 100644 index 654ff14..0000000 --- a/src/main/java/org/cryptomator/cryptolib/ecies/AuthenticatedEncryption.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import javax.crypto.AEADBadTagException; - -public interface AuthenticatedEncryption { - - /** - * AES-GCM with a 96 bit nonce taken from a the shared secret. - * - * Since the secret is derived via ECDH with an ephemeral key, the nonce is guaranteed to be unique. - */ - AuthenticatedEncryption GCM_WITH_SECRET_NONCE = new GcmWithSecretNonce(); - - /** - * @return number of bytes required during {@link #encrypt(byte[], byte[])} and {@link #decrypt(byte[], byte[])} - */ - int requiredSecretBytes(); - - /** - * Encrypts the given plaintext - * - * @param secret secret data required for encryption, such as (but not limited to) a key - * @param plaintext The data to encrypt - * @return The encrypted data, including all required information for authentication, such as a tag - */ - byte[] encrypt(byte[] secret, byte[] plaintext); - - /** - * Encrypts the given ciphertext - * - * @param secret secret data required for encryption, such as (but not limited to) a key - * @param ciphertext The data to decrypt - * @return The decrypted data - * @throws AEADBadTagException In case of an authentication failure (including wrong secret) - */ - byte[] decrypt(byte[] secret, byte[] ciphertext) throws AEADBadTagException; -} diff --git a/src/main/java/org/cryptomator/cryptolib/ecies/ECIntegratedEncryptionScheme.java b/src/main/java/org/cryptomator/cryptolib/ecies/ECIntegratedEncryptionScheme.java deleted file mode 100644 index 1df67f1..0000000 --- a/src/main/java/org/cryptomator/cryptolib/ecies/ECIntegratedEncryptionScheme.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import org.cryptomator.cryptolib.common.Destroyables; - -import javax.crypto.AEADBadTagException; -import javax.crypto.KeyAgreement; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.util.Arrays; - -public class ECIntegratedEncryptionScheme { - - /** - * The ECIES used in Cryptomator Hub: - *
    - *
  • To be used with {@link org.cryptomator.cryptolib.common.P384KeyPair P-384 EC keys}
  • - *
  • Use ANSI X9.63 KDF with SHA-256 to derive a 352 bit shared secret
  • - *
  • Cut shared secret into 256 bit key + 96 bit nonce used for AES-GCM to encrypt/decrypt
  • - *
- */ - public static final ECIntegratedEncryptionScheme HUB = new ECIntegratedEncryptionScheme(AuthenticatedEncryption.GCM_WITH_SECRET_NONCE, KeyDerivationFunction.ANSI_X963_SHA256_KDF); - - private final AuthenticatedEncryption ae; - private final KeyDerivationFunction kdf; - - public ECIntegratedEncryptionScheme(AuthenticatedEncryption ae, KeyDerivationFunction kdf) { - this.ae = ae; - this.kdf = kdf; - } - - public EncryptedMessage encrypt(KeyPairGenerator ephKeyGen, ECPublicKey receiverPublicKey, byte[] plaintext) { - KeyPair ephKeyPair = ephKeyGen.generateKeyPair(); - try { - if (ephKeyPair.getPrivate() instanceof ECPrivateKey) { - assert ephKeyPair.getPublic() instanceof ECPublicKey; - byte[] ciphertext = encrypt((ECPrivateKey) ephKeyPair.getPrivate(), receiverPublicKey, plaintext); - return new EncryptedMessage((ECPublicKey) ephKeyPair.getPublic(), ciphertext); - } else { - throw new IllegalArgumentException("key generator didn't create EC key pair"); - } - } finally { - Destroyables.destroySilently(ephKeyPair.getPrivate()); - } - } - - public byte[] decrypt(ECPrivateKey receiverPrivateKey, EncryptedMessage encryptedMessage) throws AEADBadTagException { - return decrypt(receiverPrivateKey, encryptedMessage.getEphPublicKey(), encryptedMessage.getCiphertext()); - } - - // visible for testing - byte[] encrypt(ECPrivateKey ephPrivateKey, ECPublicKey receiverPublicKey, byte[] plaintext) { - byte[] secret = ecdhAndKdf(ephPrivateKey, receiverPublicKey, ae.requiredSecretBytes()); - return ae.encrypt(secret, plaintext); - } - - // visible for testing - byte[] decrypt(ECPrivateKey receiverPrivateKey, ECPublicKey ephPublicKey, byte[] plaintext) throws AEADBadTagException { - byte[] secret = ecdhAndKdf(receiverPrivateKey, ephPublicKey, ae.requiredSecretBytes()); - return ae.decrypt(secret, plaintext); - } - - private byte[] ecdhAndKdf(ECPrivateKey privateKey, ECPublicKey publicKey, int numBytes) { - byte[] sharedSecret = new byte[0]; - try { - KeyAgreement keyAgreement = createKeyAgreement(); - keyAgreement.init(privateKey); - keyAgreement.doPhase(publicKey, true); - sharedSecret = keyAgreement.generateSecret(); - return kdf.deriveKey(sharedSecret, numBytes); - } catch (InvalidKeyException e) { - throw new IllegalArgumentException("Invalid keys", e); - } finally { - Arrays.fill(sharedSecret, (byte) 0x00); - } - } - - private static KeyAgreement createKeyAgreement() { - try { - return KeyAgreement.getInstance("ECDH"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("ECDH not supported"); - } - } - - -} diff --git a/src/main/java/org/cryptomator/cryptolib/ecies/EncryptedMessage.java b/src/main/java/org/cryptomator/cryptolib/ecies/EncryptedMessage.java deleted file mode 100644 index 08657bc..0000000 --- a/src/main/java/org/cryptomator/cryptolib/ecies/EncryptedMessage.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import java.security.interfaces.ECPublicKey; - -public class EncryptedMessage { - - private final ECPublicKey ephPublicKey; - private final byte[] ciphertext; - - public EncryptedMessage(ECPublicKey ephPublicKey, byte[] ciphertext) { - this.ephPublicKey = ephPublicKey; - this.ciphertext = ciphertext; - } - - public ECPublicKey getEphPublicKey() { - return ephPublicKey; - } - - public byte[] getCiphertext() { - return ciphertext; - } - -} diff --git a/src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java b/src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java deleted file mode 100644 index 891ef83..0000000 --- a/src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import com.google.common.base.Throwables; -import org.cryptomator.cryptolib.common.CipherSupplier; -import org.cryptomator.cryptolib.common.DestroyableSecretKey; - -import javax.crypto.AEADBadTagException; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.spec.GCMParameterSpec; -import java.util.Arrays; - -class GcmWithSecretNonce implements AuthenticatedEncryption { - - private static final int GCM_KEY_SIZE = 32; - private static final int GCM_TAG_SIZE = 16; - private static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM - - @Override - public int requiredSecretBytes() { - return GCM_KEY_SIZE + GCM_NONCE_SIZE; - } - - @Override - 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); - } catch (IllegalBlockSizeException | BadPaddingException e) { - throw new IllegalStateException("Unexpected exception during GCM decryption.", e); - } - } - - @Override - 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); - } 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 deleted file mode 100644 index f7a6b95..0000000 --- a/src/main/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunction.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import org.cryptomator.cryptolib.common.MessageDigestSupplier; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.security.DigestException; -import java.security.MessageDigest; -import java.util.Arrays; - -@FunctionalInterface -public interface KeyDerivationFunction { - - KeyDerivationFunction ANSI_X963_SHA256_KDF = (sharedSecret, keyDataLen) -> ansiX963sha256Kdf(sharedSecret, new byte[0], keyDataLen); - - /** - * Derives a key of desired length - * - * @param sharedSecret A shared secret - * @param keyDataLen Desired key length (in bytes) - * @return key data - */ - byte[] deriveKey(byte[] sharedSecret, int keyDataLen); - - /** - * Performs ANSI-X9.63-KDF with SHA-256 - * - * @param sharedSecret A shared secret - * @param sharedInfo Additional authenticated data - * @param keyDataLen Desired key length (in bytes) - * @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(); - - // 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"; - assert keyDataLen < (1L << 32 - 1) * hashLen : "keyDataLen larger than hashLen × (2^32 − 1)"; - - ByteBuffer counter = ByteBuffer.allocate(Integer.BYTES); - assert ByteOrder.BIG_ENDIAN.equals(counter.order()); - int n = (keyDataLen + hashLen - 1) / hashLen; - byte[] buffer = new byte[n * hashLen]; - try { - for (int i = 0; i < n; i++) { - digest.update(sharedSecret); - counter.clear(); - counter.putInt(i + 1); - counter.flip(); - digest.update(counter); - digest.update(sharedInfo); - digest.digest(buffer, i * hashLen, hashLen); - } - return Arrays.copyOf(buffer, keyDataLen); - } catch (DigestException e) { - throw new IllegalStateException("Invalid digest output buffer offset", e); - } finally { - Arrays.fill(buffer, (byte) 0x00); - } - } - -} 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/main/java9/module-info.java b/src/main/java9/module-info.java new file mode 100644 index 0000000..512fb83 --- /dev/null +++ b/src/main/java9/module-info.java @@ -0,0 +1,28 @@ +import org.cryptomator.cryptolib.api.CryptorProvider; + +/** + * This module provides the highlevel cryptographic API used by Cryptomator. + * + * @uses CryptorProvider See {@link CryptorProvider#forScheme(CryptorProvider.Scheme)} + * @provides CryptorProvider Providers for {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_CTRMAC SIV/CTR-then-MAC} + * and {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_GCM SIV/GCM} + */ +module org.cryptomator.cryptolib { + requires static org.bouncycastle.provider; // will be shaded + requires static org.bouncycastle.pkix; // will be shaded + requires jdk.crypto.ec; // required at runtime for ECC + requires org.cryptomator.siv; + requires com.google.gson; + requires transitive com.google.common; + requires org.slf4j; + + exports org.cryptomator.cryptolib.api; + exports org.cryptomator.cryptolib.common; + + opens org.cryptomator.cryptolib.common to com.google.gson; + + uses CryptorProvider; + + provides CryptorProvider + with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl; +} \ No newline at end of file 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/ECKeyPairTest.java b/src/test/java/org/cryptomator/cryptolib/common/ECKeyPairTest.java index bc6ddcb..22f6390 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/ECKeyPairTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/ECKeyPairTest.java @@ -5,21 +5,36 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; +import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.EllipticCurve; public class ECKeyPairTest { @Test public void testConstructorFailsForInvalidAlgorithm() throws NoSuchAlgorithmException { KeyPair rsaKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + ECParameterSpec curveParams = Mockito.mock(ECParameterSpec.class); Assertions.assertThrows(IllegalArgumentException.class, () -> { - new ECKeyPair(rsaKeyPair); + new ECKeyPair(rsaKeyPair, curveParams); }); } + private ECParameterSpec getParamsFromPublicKey(KeyPair keyPair) { + return ((ECPublicKey)keyPair.getPublic()).getParams(); + } + @Nested @DisplayName("With undestroyed key...") public class WithUndestroyed { @@ -32,7 +47,7 @@ public class WithUndestroyed { public void setup() throws NoSuchAlgorithmException { this.keyPair1 = KeyPairGenerator.getInstance("EC").generateKeyPair(); this.keyPair2 = KeyPairGenerator.getInstance("EC").generateKeyPair(); - this.ecKeyPair = new ECKeyPair(keyPair1); + this.ecKeyPair = new ECKeyPair(keyPair1, getParamsFromPublicKey(keyPair1)); } @Test @@ -57,8 +72,8 @@ public void testDestroy() { @Test public void testEquals() { - ECKeyPair other1 = new ECKeyPair(keyPair1); - ECKeyPair other2 = new ECKeyPair(keyPair2); + ECKeyPair other1 = new ECKeyPair(keyPair1, getParamsFromPublicKey(keyPair1)); + ECKeyPair other2 = new ECKeyPair(keyPair2, getParamsFromPublicKey(keyPair2)); Assertions.assertNotSame(ecKeyPair, other1); Assertions.assertEquals(ecKeyPair, other1); Assertions.assertNotSame(ecKeyPair, other2); @@ -67,8 +82,8 @@ public void testEquals() { @Test public void testHashCode() { - ECKeyPair other1 = new ECKeyPair(keyPair1); - ECKeyPair other2 = new ECKeyPair(keyPair2); + ECKeyPair other1 = new ECKeyPair(keyPair1, getParamsFromPublicKey(keyPair1)); + ECKeyPair other2 = new ECKeyPair(keyPair2, getParamsFromPublicKey(keyPair2)); Assertions.assertEquals(ecKeyPair.hashCode(), other1.hashCode()); Assertions.assertNotEquals(ecKeyPair.hashCode(), other2.hashCode()); } @@ -85,7 +100,7 @@ public class WithDestroyed { @BeforeEach public void setup() throws NoSuchAlgorithmException { this.keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair(); - this.ecKeyPair = new ECKeyPair(keyPair); + this.ecKeyPair = new ECKeyPair(keyPair, getParamsFromPublicKey(keyPair)); this.ecKeyPair.destroy(); } @@ -111,5 +126,69 @@ public void testDestroy() { } + @Nested + @DisplayName("With invalid public key...") + public class WithInvalidPublicKey { + + private ECParameterSpec curveParams = Mockito.mock(ECParameterSpec.class); + private EllipticCurve curve = Mockito.mock(EllipticCurve.class); + private ECFieldFp field = Mockito.mock(ECFieldFp.class); + private ECPublicKey publicKey = Mockito.mock(ECPublicKey.class); + private ECPrivateKey privateKey = Mockito.mock(ECPrivateKey.class); + private KeyPair keyPair = new KeyPair(publicKey, privateKey); + + @BeforeEach + public void setup() { + Mockito.doReturn(curve).when(curveParams).getCurve(); + Mockito.doReturn(field).when(curve).getField(); + Mockito.doReturn(BigInteger.ZERO).when(curve).getA(); + Mockito.doReturn(BigInteger.ZERO).when(curve).getB(); + Mockito.doReturn(1).when(curveParams).getCofactor(); + Mockito.doReturn(new ECPoint(BigInteger.ONE, BigInteger.ONE)).when(publicKey).getW(); + Mockito.doReturn(BigInteger.valueOf(2)).when(field).getP(); + } + + @Test + public void testValid() { + Assertions.assertDoesNotThrow(() -> new ECKeyPair(keyPair, curveParams)); + } + + @Test + public void testUnsupportedCofactor() { + Mockito.doReturn(2).when(curveParams).getCofactor(); + Assertions.assertThrows(IllegalArgumentException.class, () -> new ECKeyPair(keyPair, curveParams)); + } + + @Test + public void testInfiniteW() { + Mockito.doReturn(ECPoint.POINT_INFINITY).when(publicKey).getW(); + Assertions.assertThrows(IllegalArgumentException.class, () -> new ECKeyPair(keyPair, curveParams)); + } + + @ParameterizedTest + @CsvSource(value = { + "-1, 0", + "0, -1", + "2, 0", + "0, 2", + }) + public void testInvalidPoint(int x, int y) { + Mockito.doReturn(new ECPoint(BigInteger.valueOf(x), BigInteger.valueOf(y))).when(publicKey).getW(); + Assertions.assertThrows(IllegalArgumentException.class, () -> new ECKeyPair(keyPair, curveParams)); + } + + @ParameterizedTest + @CsvSource(value = { + "1, 0", + "0, 1", + }) + public void testInvalidCurveCoefficients(int a, int b) { + Mockito.doReturn(BigInteger.valueOf(a)).when(curve).getA(); + Mockito.doReturn(BigInteger.valueOf(b)).when(curve).getB(); + Assertions.assertThrows(IllegalArgumentException.class, () -> new ECKeyPair(keyPair, curveParams)); + } + + } + } \ No newline at end of file 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/MasterkeyHubAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyHubAccessTest.java deleted file mode 100644 index 7b779ef..0000000 --- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyHubAccessTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.cryptomator.cryptolib.common; - -import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptolib.api.Masterkey; -import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -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 java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.interfaces.ECPrivateKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Arrays; - -public class MasterkeyHubAccessTest { - - private ECPrivateKey devicePrivateKey; - - @BeforeEach - public void setup() throws NoSuchAlgorithmException, InvalidKeySpecException { - byte[] keyBytes = BaseEncoding.base64Url().decode("ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDDzj9mBnoqoYTO0wQDvM2iyI2wrNe468US1mHMjdJcKWGGvky4pMexIvmvmDsZLdsY"); - this.devicePrivateKey = (ECPrivateKey) KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); - } - - @Test - @DisplayName("decryptMasterkey(...)") - public void testDecrypt() { - String ephPk = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkcv3x-hkCnb8Kr8TNfaLpD4q64ZqPn4p1yuM8r2r16h6f6mG01kFBp2EoY575bCcmT54PxiDFkf3KKqHXFjZwBhdm6zMp22l37ZlmKyHG96vkB7Rh6qFyzEhSQ_nvl2G"; - String ciphertext = "KQ48XS6ziW3tS7SMLR5sc2o_Y80OR4SS_htHpk8SHn4KrqI07EtDFFbNJ9AcNOazSu3TXrml--t_bEXprfnPqa3MlBvmPUVBcwUFJPDTR9Y"; - - Masterkey masterkey = MasterkeyHubAccess.decryptMasterkey(devicePrivateKey, ciphertext, ephPk); - - byte[] expectedKey = new byte[64]; - Arrays.fill(expectedKey, 0, 32, (byte) 0x55); - Arrays.fill(expectedKey, 32, 64, (byte) 0x77); - Assertions.assertArrayEquals(expectedKey, masterkey.getEncoded()); - } - - @Test - @DisplayName("decryptMasterkey(...) with tampered ephemeral public key") - public void testDecryptWithInvalidEphemeralPublicKey() { - String ephPk = "mHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkcv3x-hkCnb8Kr8TNfaLpD4q64ZqPn4p1yuM8r2r16h6f6mG01kFBp2EoY575bCcmT54PxiDFkf3KKqHXFjZwBhdm6zMp22l37ZlmKyHG96vkB7Rh6qFyzEhSQ_nvl2G"; - String ciphertext = "KQ48XS6ziW3tS7SMLR5sc2o_Y80OR4SS_htHpk8SHn4KrqI07EtDFFbNJ9AcNOazSu3TXrml--t_bEXprfnPqa3MlBvmPUVBcwUFJPDTR9Y"; - - Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { - MasterkeyHubAccess.decryptMasterkey(devicePrivateKey, ciphertext, ephPk); - }); - } - - @Test - @DisplayName("decryptMasterkey(...) with tampered ciphertext") - public void testDecryptWithInvalidCiphertext() { - String ephPk = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkcv3x-hkCnb8Kr8TNfaLpD4q64ZqPn4p1yuM8r2r16h6f6mG01kFBp2EoY575bCcmT54PxiDFkf3KKqHXFjZwBhdm6zMp22l37ZlmKyHG96vkB7Rh6qFyzEhSQ_nvl2G"; - String ciphertext = "kQ48XS6ziW3tS7SMLR5sc2o_Y80OR4SS_htHpk8SHn4KrqI07EtDFFbNJ9AcNOazSu3TXrml--t_bEXprfnPqa3MlBvmPUVBcwUFJPDTR9Y"; - - Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { - MasterkeyHubAccess.decryptMasterkey(devicePrivateKey, ciphertext, ephPk); - }); - } - - @Test - @DisplayName("decryptMasterkey(...) with invalid device key") - public void testDecryptWithInvalidDeviceKey() { - String ephPk = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkcv3x-hkCnb8Kr8TNfaLpD4q64ZqPn4p1yuM8r2r16h6f6mG01kFBp2EoY575bCcmT54PxiDFkf3KKqHXFjZwBhdm6zMp22l37ZlmKyHG96vkB7Rh6qFyzEhSQ_nvl2G"; - String ciphertext = "KQ48XS6ziW3tS7SMLR5sc2o_Y80OR4SS_htHpk8SHn4KrqI07EtDFFbNJ9AcNOazSu3TXrml--t_bEXprfnPqa3MlBvmPUVBcwUFJPDTR9Y"; - ECPrivateKey wrongKey = P384KeyPair.generate().getPrivate(); - - Assertions.assertThrows(MasterkeyLoadingFailedException.class, () -> { - MasterkeyHubAccess.decryptMasterkey(wrongKey, ciphertext, ephPk); - }); - } - -} \ No newline at end of file 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/P384KeyPairTest.java b/src/test/java/org/cryptomator/cryptolib/common/P384KeyPairTest.java index 13a8aaa..a3c2cea 100644 --- a/src/test/java/org/cryptomator/cryptolib/common/P384KeyPairTest.java +++ b/src/test/java/org/cryptomator/cryptolib/common/P384KeyPairTest.java @@ -9,6 +9,10 @@ import java.io.IOException; import java.nio.file.Path; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; public class P384KeyPairTest { @@ -22,6 +26,16 @@ public void testGenerate() { Assertions.assertNotEquals(keyPair1, keyPair2); } + @Test + @DisplayName("create()") + public void testCreate() throws InvalidKeySpecException { + X509EncodedKeySpec publicKey = new X509EncodedKeySpec(Base64.getDecoder().decode("MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu")); + PKCS8EncodedKeySpec privateKey = new PKCS8EncodedKeySpec(Base64.getDecoder().decode("ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ")); + + P384KeyPair keyPair = P384KeyPair.create(publicKey, privateKey); + Assertions.assertNotNull(keyPair); + } + @Test @DisplayName("store(...)") public void testStore(@TempDir Path tmpDir) { 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/ECIntegratedEncryptionSchemeTest.java b/src/test/java/org/cryptomator/cryptolib/ecies/ECIntegratedEncryptionSchemeTest.java deleted file mode 100644 index df836a5..0000000 --- a/src/test/java/org/cryptomator/cryptolib/ecies/ECIntegratedEncryptionSchemeTest.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import javax.crypto.AEADBadTagException; -import javax.crypto.KeyAgreement; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.util.Arrays; - -public class ECIntegratedEncryptionSchemeTest { - - @Nested - @DisplayName("With null encryption scheme...") - public class WithIdentityCipher { - - private AuthenticatedEncryption ae; - private KeyDerivationFunction kdf; - private ECIntegratedEncryptionScheme ecies; - private KeyPair ephemeral; - private KeyPair receiver; - private byte[] expectedSharedSecret; - private byte[] derivedSecret; - - @BeforeEach - public void setup() throws AEADBadTagException, NoSuchAlgorithmException, InvalidKeyException { - this.ephemeral = KeyPairGenerator.getInstance("EC").generateKeyPair(); - this.receiver = KeyPairGenerator.getInstance("EC").generateKeyPair(); - KeyAgreement ecdh = KeyAgreement.getInstance("ECDH"); - ecdh.init(ephemeral.getPrivate()); - ecdh.doPhase(receiver.getPublic(), true); - this.expectedSharedSecret = ecdh.generateSecret(); - this.ae = Mockito.mock(AuthenticatedEncryption.class); - this.kdf = Mockito.mock(KeyDerivationFunction.class); - this.ecies = new ECIntegratedEncryptionScheme(ae, kdf); - this.derivedSecret = new byte[32]; - Arrays.fill(derivedSecret, (byte) 0xAA); - - // set up null encryption - Mockito.doReturn(32).when(ae).requiredSecretBytes(); - Mockito.doAnswer(invocation -> invocation.getArgument(1)).when(ae).encrypt(Mockito.any(), Mockito.any()); - Mockito.doAnswer(invocation -> invocation.getArgument(1)).when(ae).decrypt(Mockito.any(), Mockito.any()); - - // set up null KDF - Mockito.doReturn(derivedSecret).when(kdf).deriveKey(expectedSharedSecret, 32); - } - - @Test - public void testEncryptWithInvalidKey() { - ECPrivateKey invalidSk = Mockito.mock(ECPrivateKey.class); - ECPublicKey validPk = (ECPublicKey) receiver.getPublic(); - Mockito.doReturn("WRONG").when(invalidSk).getAlgorithm(); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - ecies.encrypt(invalidSk, validPk, new byte[42]); - }); - } - - @Test - public void testEncrypt() { - byte[] cleartext = "secret message".getBytes(StandardCharsets.UTF_8); - - byte[] ciphertext = ecies.encrypt((ECPrivateKey) ephemeral.getPrivate(), (ECPublicKey) receiver.getPublic(), cleartext); - - Assertions.assertArrayEquals(cleartext, ciphertext); - Mockito.verify(kdf).deriveKey(Mockito.any(), Mockito.eq(32)); - Mockito.verify(ae).encrypt(derivedSecret, cleartext); - } - - @Test - public void testDecrypt() throws AEADBadTagException { - byte[] ciphertext = "secret message".getBytes(StandardCharsets.UTF_8); - - byte[] cleartext = ecies.decrypt((ECPrivateKey) receiver.getPrivate(), (ECPublicKey) ephemeral.getPublic(), ciphertext); - - Assertions.assertArrayEquals(ciphertext, cleartext); - Mockito.verify(kdf).deriveKey(Mockito.any(), Mockito.eq(32)); - Mockito.verify(ae).decrypt(derivedSecret, cleartext); - } - - } - - @Nested - @DisplayName("With Cryptomator Hub encryption scheme...") - public class WithHubScheme { - - private ECIntegratedEncryptionScheme ecies = ECIntegratedEncryptionScheme.HUB; - private KeyPairGenerator keyGen; - private KeyPair receiverKeyPair; - - @BeforeEach - public void setup() throws NoSuchAlgorithmException { - this.keyGen = KeyPairGenerator.getInstance("EC"); - this.receiverKeyPair = keyGen.generateKeyPair(); - } - - @Test - @DisplayName("encrypt(...)") - public void testEncrypt() { - byte[] cleartext = "hello world".getBytes(StandardCharsets.UTF_8); - EncryptedMessage msg = ecies.encrypt(keyGen, (ECPublicKey) receiverKeyPair.getPublic(), cleartext); - Assertions.assertNotNull(msg.getCiphertext()); - Assertions.assertNotNull(msg.getEphPublicKey()); - } - - @Test - @DisplayName("encrypt(...) with invalid keygen") - public void testEncryptWithInvalidKeyGen() throws NoSuchAlgorithmException { - KeyPairGenerator rsaKeyGen = KeyPairGenerator.getInstance("RSA"); - byte[] cleartext = "hello world".getBytes(StandardCharsets.UTF_8); - ECPublicKey receiverPublicKey = (ECPublicKey) receiverKeyPair.getPublic(); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - ecies.encrypt(rsaKeyGen, receiverPublicKey, cleartext); - }); - } - - @Nested - @DisplayName("With Cryptomator Hub encryption scheme...") - public class WithEncryptedMessage { - - private byte[] expectedCleartext = "hello world".getBytes(StandardCharsets.UTF_8); - private EncryptedMessage encryptedMessage; - - @BeforeEach - public void setup() throws NoSuchAlgorithmException { - byte[] cleartext = "hello world".getBytes(StandardCharsets.UTF_8); - this.encryptedMessage = ecies.encrypt(keyGen, (ECPublicKey) receiverKeyPair.getPublic(), cleartext); - } - - @Test - @DisplayName("decrypt(...)") - public void testDecrypt() throws AEADBadTagException { - byte[] cleartext = ecies.decrypt((ECPrivateKey) receiverKeyPair.getPrivate(), encryptedMessage); - Assertions.assertArrayEquals(expectedCleartext, cleartext); - } - - @Test - @DisplayName("decrypt(...) with invalid key") - public void testDecryptWithInvalidKey() { - ECPrivateKey wrongPrivateKey = (ECPrivateKey) keyGen.generateKeyPair().getPrivate(); - Assertions.assertThrows(AEADBadTagException.class, () -> { - ecies.decrypt(wrongPrivateKey, encryptedMessage); - }); - } - - } - - } - -} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonceTest.java b/src/test/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonceTest.java deleted file mode 100644 index 91ed77a..0000000 --- a/src/test/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonceTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import org.cryptomator.cryptolib.common.CipherSupplier; -import org.cryptomator.cryptolib.common.GcmTestHelper; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.converter.ConvertWith; -import org.junit.jupiter.params.provider.CsvSource; - -import javax.crypto.AEADBadTagException; - -/** - * Test vectors from https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf - */ -public class GcmWithSecretNonceTest { - - private final GcmWithSecretNonce ae = new GcmWithSecretNonce(); - - @BeforeEach - public void setup() { - // reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse - GcmTestHelper.reset((mode, key, params) -> { - CipherSupplier.AES_GCM.forEncryption(key, params); - }); - } - - @Test - public void testRequiredSecretBytes() { - Assertions.assertEquals(44, ae.requiredSecretBytes()); - } - - @ParameterizedTest - @CsvSource(value = { - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, , 530f8afbc74536b9a963b4f1c4cb738b", // test case 13 - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, 00000000000000000000000000000000, cea7403d4d606b6e074ec5d3baf39d18d0d1c8a799996bf0265b98b5d48ab919", // test case 14 - "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308cafebabefacedbaddecaf888, d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255, 522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015adb094dac5d93471bdec1a502270e3cc6c", // test case 15 - }) - public void testEncrypt(@ConvertWith(HexConverter.class) byte[] secret, @ConvertWith(HexConverter.class) byte[] plaintext, @ConvertWith(HexConverter.class) byte[] expectedCiphertext) { - byte[] ciphertext = ae.encrypt(secret, plaintext); - Assertions.assertArrayEquals(expectedCiphertext, ciphertext); - } - - @ParameterizedTest - @CsvSource(value = { - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, , 530f8afbc74536b9a963b4f1c4cb738b", // test case 13 - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, 00000000000000000000000000000000, cea7403d4d606b6e074ec5d3baf39d18d0d1c8a799996bf0265b98b5d48ab919", // test case 14 - "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308cafebabefacedbaddecaf888, d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255, 522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015adb094dac5d93471bdec1a502270e3cc6c", // test case 15 - }) - public void testDecrypt(@ConvertWith(HexConverter.class) byte[] secret, @ConvertWith(HexConverter.class) byte[] expectedPlaintext, @ConvertWith(HexConverter.class) byte[] ciphertext) throws AEADBadTagException { - byte[] plaintext = ae.decrypt(secret, ciphertext); - Assertions.assertArrayEquals(expectedPlaintext, plaintext); - } - - - @ParameterizedTest - @CsvSource(value = { - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, 530f8afbc74536b9a963b4f1c4cb738b", - "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000, 530f8afbc74536b9a963b4f1c4cb738b", - }) - public void testDecryptInvalid(@ConvertWith(HexConverter.class) byte[] secret, @ConvertWith(HexConverter.class) byte[] ciphertext) { - Assertions.assertThrows(AEADBadTagException.class, () -> { - ae.decrypt(secret, ciphertext); - }); - } - -} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/ecies/HexConverter.java b/src/test/java/org/cryptomator/cryptolib/ecies/HexConverter.java deleted file mode 100644 index 2daf59e..0000000 --- a/src/test/java/org/cryptomator/cryptolib/ecies/HexConverter.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import com.google.common.io.BaseEncoding; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.params.converter.ArgumentConversionException; -import org.junit.jupiter.params.converter.ArgumentConverter; - -class HexConverter implements ArgumentConverter { - - @Override - public byte[] convert(Object source, ParameterContext context) throws ArgumentConversionException { - if (source == null) { - return new byte[0]; - } else if (source instanceof String) { - return BaseEncoding.base16().lowerCase().decode((String) source); - } else { - return null; - } - } -} diff --git a/src/test/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunctionTest.java b/src/test/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunctionTest.java deleted file mode 100644 index 6395dc9..0000000 --- a/src/test/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunctionTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.cryptomator.cryptolib.ecies; - -import com.google.common.io.BaseEncoding; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.converter.ConvertWith; -import org.junit.jupiter.params.provider.CsvSource; - -public class KeyDerivationFunctionTest { - - @DisplayName("ANSI-X9.63-KDF") - @ParameterizedTest - @CsvSource(value = { - "96c05619d56c328ab95fe84b18264b08725b85e33fd34f08, , 16, 443024c3dae66b95e6f5670601558f71", - "96f600b73ad6ac5629577eced51743dd2c24c21b1ac83ee4, , 16, b6295162a7804f5667ba9070f82fa522", - "22518b10e70f2a3f243810ae3254139efbee04aa57c7af7d, 75eef81aa3041e33b80971203d2c0c52, 128, c498af77161cc59f2962b9a713e2b215152d139766ce34a776df11866a69bf2e52a13d9c7c6fc878c50c5ea0bc7b00e0da2447cfd874f6cf92f30d0097111485500c90c3af8b487872d04685d14c8d1dc8d7fa08beb0ce0ababc11f0bd496269142d43525a78e5bc79a17f59676a5706dc54d54d4d1f0bd7e386128ec26afc21", - "7e335afa4b31d772c0635c7b0e06f26fcd781df947d2990a, d65a4812733f8cdbcdfb4b2f4c191d87, 128, c0bd9e38a8f9de14c2acd35b2f3410c6988cf02400543631e0d6a4c1d030365acbf398115e51aaddebdc9590664210f9aa9fed770d4c57edeafa0b8c14f93300865251218c262d63dadc47dfa0e0284826793985137e0a544ec80abf2fdf5ab90bdaea66204012efe34971dc431d625cd9a329b8217cc8fd0d9f02b13f2f6b0b", - }) - // test vectors from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/components/800-135testvectors/ansx963_2001.zip - public void testAnsiX963sha256(@ConvertWith(HexConverter.class) byte[] sharedSecret, @ConvertWith(HexConverter.class) byte[] sharedInfo, int outLen, @ConvertWith(HexConverter.class) byte[] expectedResult) { - byte[] result = KeyDerivationFunction.ansiX963sha256Kdf(sharedSecret, sharedInfo, outLen); - Assertions.assertArrayEquals(expectedResult, result); - } - - @DisplayName("kdf.deriveKey()") - @Test - public void testDeriveKey() { - byte[] secret = BaseEncoding.base16().lowerCase().decode("96c05619d56c328ab95fe84b18264b08725b85e33fd34f08"); - - byte[] result = KeyDerivationFunction.ANSI_X963_SHA256_KDF.deriveKey(secret, 16); - - byte[] expected = BaseEncoding.base16().lowerCase().decode("443024c3dae66b95e6f5670601558f71"); - Assertions.assertArrayEquals(expected, result); - } - -} \ No newline at end of file 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); }); } diff --git a/suppression.xml b/suppression.xml index c714921..ebb877b 100644 --- a/suppression.xml +++ b/suppression.xml @@ -1,4 +1,12 @@ - + + + + org\.cryptomator:.* + cpe:/a:cryptomator:cryptomator + CVE-2022-25366 + \ No newline at end of file