Skip to content

Commit b5ed6d8

Browse files
authored
Add fix for NPE on encrypt/decrypt (#170)
* Add fix for NPE on encrypt/decrypt * Remove useless test * Bump versions
1 parent 3f85351 commit b5ed6d8

6 files changed

Lines changed: 161 additions & 6 deletions

File tree

examples/cached-key-example/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<dependency>
3131
<groupId>com.ironcorelabs</groupId>
3232
<artifactId>tenant-security-java</artifactId>
33-
<version>8.1.0</version>
33+
<version>8.1.1</version>
3434
</dependency>
3535
</dependencies>
3636

@@ -82,4 +82,4 @@
8282
</plugin>
8383
</plugins>
8484
</build>
85-
</project>
85+
</project>

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<groupId>com.ironcorelabs</groupId>
99
<artifactId>tenant-security-java</artifactId>
1010
<packaging>jar</packaging>
11-
<version>8.1.0</version>
11+
<version>8.1.1</version>
1212
<name>tenant-security-java</name>
1313
<url>https://ironcorelabs.com/docs</url>
1414
<description>Java client library for the IronCore Labs Tenant Security Proxy.</description>

src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtils.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,12 @@ public static CompletableFuture<Void> encryptStreamInternal(byte[] documentKey,
7777
output.write(headerBytes);
7878
output.write(iv);
7979
while ((bytesRead = readNBytes(input, STREAM_CHUNKING)).length != 0) {
80+
// Cipher.update may return null per its Javadoc. BC-FIPS buffers AEAD data until
81+
// doFinal so ciphertext and tag are released together.
8082
byte[] encryptedBytes = cipher.update(bytesRead);
81-
output.write(encryptedBytes);
83+
if (encryptedBytes != null) {
84+
output.write(encryptedBytes);
85+
}
8286
}
8387
// Final bytes, which might be buffered data or just the GCM tag.
8488
byte[] finalBytes = cipher.doFinal();
@@ -104,7 +108,10 @@ public static CompletableFuture<Void> decryptStreamInternal(byte[] documentKey,
104108
Cipher cipher = getNewAesCipher(documentKey, iv, false);
105109
byte[] currentChunk = new byte[0];
106110
while ((currentChunk = readNBytes(encryptedStream, STREAM_CHUNKING)).length > 0) {
107-
decryptedStream.write(cipher.update(currentChunk));
111+
byte[] decryptedChunk = cipher.update(currentChunk);
112+
if (decryptedChunk != null) {
113+
decryptedStream.write(decryptedChunk);
114+
}
108115
}
109116
decryptedStream.write(cipher.doFinal());
110117
return null;

src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/TenantSecurityRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private static String stripTrailingSlash(String s) {
6161
private final int connectTimeout;
6262

6363
// TSC version that will be sent to the TSP.
64-
static final String sdkVersion = "8.1.0";
64+
static final String sdkVersion = "8.1.1";
6565

6666
TenantSecurityRequest(String tspDomain, String apiKey, int requestThreadSize, int timeout) {
6767
this(tspDomain, apiKey, requestThreadSize, timeout, timeout);
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package com.ironcorelabs.tenantsecurity.kms.v1;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.security.AlgorithmParameters;
5+
import java.security.InvalidAlgorithmParameterException;
6+
import java.security.InvalidKeyException;
7+
import java.security.Key;
8+
import java.security.Provider;
9+
import java.security.SecureRandom;
10+
import java.security.spec.AlgorithmParameterSpec;
11+
import javax.crypto.BadPaddingException;
12+
import javax.crypto.Cipher;
13+
import javax.crypto.CipherSpi;
14+
import javax.crypto.IllegalBlockSizeException;
15+
import javax.crypto.ShortBufferException;
16+
17+
// Test-only JCE provider used to reproduce the BC-FIPS AEAD buffering behavior
18+
// that surfaces in issue #167. Every engineUpdate call buffers input and returns
19+
// null; buffered bytes are released only at engineDoFinal. See
20+
// CryptoUtilsTest#streamingRoundtripWithBufferingProvider.
21+
public class BufferingGcmProvider extends Provider {
22+
public BufferingGcmProvider() {
23+
super("BufferingGcmTest", "1.0", "Test provider buffering GCM data like BC-FIPS");
24+
put("Cipher.AES/GCM/NoPadding", BufferingGcmCipherSpi.class.getName());
25+
}
26+
27+
public static class BufferingGcmCipherSpi extends CipherSpi {
28+
private final Cipher delegate;
29+
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
30+
31+
public BufferingGcmCipherSpi() throws Exception {
32+
delegate = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
33+
}
34+
35+
@Override
36+
protected void engineSetMode(String mode) {}
37+
38+
@Override
39+
protected void engineSetPadding(String padding) {}
40+
41+
@Override
42+
protected int engineGetBlockSize() {
43+
return delegate.getBlockSize();
44+
}
45+
46+
@Override
47+
protected int engineGetOutputSize(int inputLen) {
48+
return delegate.getOutputSize(inputLen + buffer.size());
49+
}
50+
51+
@Override
52+
protected byte[] engineGetIV() {
53+
return delegate.getIV();
54+
}
55+
56+
@Override
57+
protected AlgorithmParameters engineGetParameters() {
58+
return delegate.getParameters();
59+
}
60+
61+
@Override
62+
protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException {
63+
buffer.reset();
64+
delegate.init(opmode, key, random);
65+
}
66+
67+
@Override
68+
protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params,
69+
SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException {
70+
buffer.reset();
71+
delegate.init(opmode, key, params, random);
72+
}
73+
74+
@Override
75+
protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random)
76+
throws InvalidKeyException, InvalidAlgorithmParameterException {
77+
buffer.reset();
78+
delegate.init(opmode, key, params, random);
79+
}
80+
81+
@Override
82+
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
83+
if (input != null && inputLen > 0) {
84+
buffer.write(input, inputOffset, inputLen);
85+
}
86+
return null;
87+
}
88+
89+
@Override
90+
protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output,
91+
int outputOffset) {
92+
if (input != null && inputLen > 0) {
93+
buffer.write(input, inputOffset, inputLen);
94+
}
95+
return 0;
96+
}
97+
98+
@Override
99+
protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen)
100+
throws IllegalBlockSizeException, BadPaddingException {
101+
if (input != null && inputLen > 0) {
102+
buffer.write(input, inputOffset, inputLen);
103+
}
104+
byte[] all = buffer.toByteArray();
105+
buffer.reset();
106+
return delegate.doFinal(all);
107+
}
108+
109+
@Override
110+
protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output,
111+
int outputOffset)
112+
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
113+
byte[] result = engineDoFinal(input, inputOffset, inputLen);
114+
if (output.length - outputOffset < result.length) {
115+
throw new ShortBufferException();
116+
}
117+
System.arraycopy(result, 0, output, outputOffset, result.length);
118+
return result.length;
119+
}
120+
}
121+
}

src/test/java/com/ironcorelabs/tenantsecurity/kms/v1/CryptoUtilsTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.ironcorelabs.tenantsecurity.kms.v1;
22

33
import static org.testng.Assert.assertEquals;
4+
import static org.testng.Assert.assertNull;
45

56
import java.io.ByteArrayInputStream;
67
import java.io.ByteArrayOutputStream;
78
import java.nio.ByteBuffer;
89
import java.security.SecureRandom;
910
import java.util.Arrays;
1011
import java.util.stream.IntStream;
12+
import javax.crypto.Cipher;
1113
import org.testng.annotations.Test;
1214

1315
@Test(groups = {"unit"})
@@ -259,4 +261,29 @@ public void getNBytesRequestMoreOnEmpty() throws Exception {
259261
byte[] buffer = new byte[0];
260262
assertEquals(CryptoUtils.readNBytes(new ByteArrayInputStream(buffer), 10), new byte[0]);
261263
}
264+
265+
// Regression for issue #167. Simulates BC-FIPS GCM behavior with a custom JCE
266+
// provider that buffers AEAD data and returns null from every Cipher.update call
267+
// until doFinal. Before the fix, encryptStreamInternal NPE'd on output.write(null).
268+
public void streamingRoundtripWithBufferingProvider() throws Exception {
269+
java.security.Provider provider = new BufferingGcmProvider();
270+
java.security.Security.insertProviderAt(provider, 1);
271+
try {
272+
byte[] documentKey = new byte[32];
273+
secureRandom.nextBytes(documentKey);
274+
byte[] plaintext = "foo".getBytes("UTF-8");
275+
ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintext);
276+
ByteArrayOutputStream encryptOutputStream = new ByteArrayOutputStream();
277+
CryptoUtils.encryptStreamInternal(documentKey, metadata, inputStream, encryptOutputStream,
278+
secureRandom).get();
279+
byte[] encryptedBytes = encryptOutputStream.toByteArray();
280+
ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream();
281+
ByteArrayInputStream encryptedStream = new ByteArrayInputStream(encryptedBytes);
282+
CryptoUtils.decryptStreamInternal(documentKey, encryptedStream, decryptedStream).get();
283+
assertEquals(decryptedStream.toByteArray(), plaintext);
284+
} finally {
285+
java.security.Security.removeProvider(provider.getName());
286+
}
287+
}
288+
262289
}

0 commit comments

Comments
 (0)