diff --git a/examples/cached-key-example/pom.xml b/examples/cached-key-example/pom.xml index ff44087..f78ebe1 100644 --- a/examples/cached-key-example/pom.xml +++ b/examples/cached-key-example/pom.xml @@ -30,7 +30,7 @@ com.ironcorelabs tenant-security-java - 8.1.0 + 8.1.1 @@ -82,4 +82,4 @@ - \ No newline at end of file + diff --git a/pom.xml b/pom.xml index d60c4e4..5b90ba3 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.ironcorelabs tenant-security-java jar - 8.1.0 + 8.1.1 tenant-security-java https://ironcorelabs.com/docs Java client library for the IronCore Labs Tenant Security Proxy. diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtils.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtils.java index dcf17fb..612e890 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtils.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtils.java @@ -77,8 +77,12 @@ public static CompletableFuture encryptStreamInternal(byte[] documentKey, output.write(headerBytes); output.write(iv); while ((bytesRead = readNBytes(input, STREAM_CHUNKING)).length != 0) { + // Cipher.update may return null per its Javadoc. BC-FIPS buffers AEAD data until + // doFinal so ciphertext and tag are released together. byte[] encryptedBytes = cipher.update(bytesRead); - output.write(encryptedBytes); + if (encryptedBytes != null) { + output.write(encryptedBytes); + } } // Final bytes, which might be buffered data or just the GCM tag. byte[] finalBytes = cipher.doFinal(); @@ -104,7 +108,10 @@ public static CompletableFuture decryptStreamInternal(byte[] documentKey, Cipher cipher = getNewAesCipher(documentKey, iv, false); byte[] currentChunk = new byte[0]; while ((currentChunk = readNBytes(encryptedStream, STREAM_CHUNKING)).length > 0) { - decryptedStream.write(cipher.update(currentChunk)); + byte[] decryptedChunk = cipher.update(currentChunk); + if (decryptedChunk != null) { + decryptedStream.write(decryptedChunk); + } } decryptedStream.write(cipher.doFinal()); return null; diff --git a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java index 60ad0ce..138fda5 100644 --- a/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java +++ b/src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java @@ -61,7 +61,7 @@ private static String stripTrailingSlash(String s) { private final int connectTimeout; // TSC version that will be sent to the TSP. - static final String sdkVersion = "8.1.0"; + static final String sdkVersion = "8.1.1"; TenantSecurityRequest(String tspDomain, String apiKey, int requestThreadSize, int timeout) { this(tspDomain, apiKey, requestThreadSize, timeout, timeout); diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/BufferingGcmProvider.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/BufferingGcmProvider.java new file mode 100644 index 0000000..7a8653c --- /dev/null +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/BufferingGcmProvider.java @@ -0,0 +1,121 @@ +package com.ironcorelabs.tenantsecurity.kms.v1; + +import java.io.ByteArrayOutputStream; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; + +// Test-only JCE provider used to reproduce the BC-FIPS AEAD buffering behavior +// that surfaces in issue #167. Every engineUpdate call buffers input and returns +// null; buffered bytes are released only at engineDoFinal. See +// CryptoUtilsTest#streamingRoundtripWithBufferingProvider. +public class BufferingGcmProvider extends Provider { + public BufferingGcmProvider() { + super("BufferingGcmTest", "1.0", "Test provider buffering GCM data like BC-FIPS"); + put("Cipher.AES/GCM/NoPadding", BufferingGcmCipherSpi.class.getName()); + } + + public static class BufferingGcmCipherSpi extends CipherSpi { + private final Cipher delegate; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + public BufferingGcmCipherSpi() throws Exception { + delegate = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE"); + } + + @Override + protected void engineSetMode(String mode) {} + + @Override + protected void engineSetPadding(String padding) {} + + @Override + protected int engineGetBlockSize() { + return delegate.getBlockSize(); + } + + @Override + protected int engineGetOutputSize(int inputLen) { + return delegate.getOutputSize(inputLen + buffer.size()); + } + + @Override + protected byte[] engineGetIV() { + return delegate.getIV(); + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return delegate.getParameters(); + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + buffer.reset(); + delegate.init(opmode, key, random); + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, + SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + buffer.reset(); + delegate.init(opmode, key, params, random); + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + buffer.reset(); + delegate.init(opmode, key, params, random); + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + if (input != null && inputLen > 0) { + buffer.write(input, inputOffset, inputLen); + } + return null; + } + + @Override + protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, + int outputOffset) { + if (input != null && inputLen > 0) { + buffer.write(input, inputOffset, inputLen); + } + return 0; + } + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + if (input != null && inputLen > 0) { + buffer.write(input, inputOffset, inputLen); + } + byte[] all = buffer.toByteArray(); + buffer.reset(); + return delegate.doFinal(all); + } + + @Override + protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, + int outputOffset) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + byte[] result = engineDoFinal(input, inputOffset, inputLen); + if (output.length - outputOffset < result.length) { + throw new ShortBufferException(); + } + System.arraycopy(result, 0, output, outputOffset, result.length); + return result.length; + } + } +} diff --git a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtilsTest.java b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtilsTest.java index 45c3da0..a8e0dfb 100644 --- a/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtilsTest.java +++ b/src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtilsTest.java @@ -1,6 +1,7 @@ package com.ironcorelabs.tenantsecurity.kms.v1; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -8,6 +9,7 @@ import java.security.SecureRandom; import java.util.Arrays; import java.util.stream.IntStream; +import javax.crypto.Cipher; import org.testng.annotations.Test; @Test(groups = {"unit"}) @@ -259,4 +261,29 @@ public void getNBytesRequestMoreOnEmpty() throws Exception { byte[] buffer = new byte[0]; assertEquals(CryptoUtils.readNBytes(new ByteArrayInputStream(buffer), 10), new byte[0]); } + + // Regression for issue #167. Simulates BC-FIPS GCM behavior with a custom JCE + // provider that buffers AEAD data and returns null from every Cipher.update call + // until doFinal. Before the fix, encryptStreamInternal NPE'd on output.write(null). + public void streamingRoundtripWithBufferingProvider() throws Exception { + java.security.Provider provider = new BufferingGcmProvider(); + java.security.Security.insertProviderAt(provider, 1); + try { + byte[] documentKey = new byte[32]; + secureRandom.nextBytes(documentKey); + byte[] plaintext = "foo".getBytes("UTF-8"); + ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintext); + ByteArrayOutputStream encryptOutputStream = new ByteArrayOutputStream(); + CryptoUtils.encryptStreamInternal(documentKey, metadata, inputStream, encryptOutputStream, + secureRandom).get(); + byte[] encryptedBytes = encryptOutputStream.toByteArray(); + ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream(); + ByteArrayInputStream encryptedStream = new ByteArrayInputStream(encryptedBytes); + CryptoUtils.decryptStreamInternal(documentKey, encryptedStream, decryptedStream).get(); + assertEquals(decryptedStream.toByteArray(), plaintext); + } finally { + java.security.Security.removeProvider(provider.getName()); + } + } + }