Skip to content

Commit 45ef67c

Browse files
committed
Test AES-CTR encrypt/decrypt round trip
1 parent c45fb91 commit 45ef67c

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

crypto-core/src/main/java/com/palantir/crypto2/cipher/Jdk8292158.java

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
import com.google.common.base.Suppliers;
2222
import com.google.common.collect.ImmutableSet;
2323
import com.google.common.collect.ImmutableSortedSet;
24+
import com.google.common.io.BaseEncoding;
25+
import com.palantir.logsafe.Preconditions;
2426
import com.palantir.logsafe.SafeArg;
27+
import com.palantir.logsafe.UnsafeArg;
2528
import com.palantir.logsafe.exceptions.SafeIllegalStateException;
2629
import com.palantir.logsafe.logger.SafeLogger;
2730
import com.palantir.logsafe.logger.SafeLoggerFactory;
@@ -31,14 +34,21 @@
3134
import java.nio.file.Files;
3235
import java.nio.file.Path;
3336
import java.nio.file.Paths;
37+
import java.security.GeneralSecurityException;
38+
import java.security.NoSuchProviderException;
3439
import java.util.Arrays;
3540
import java.util.Comparator;
3641
import java.util.Objects;
42+
import java.util.Random;
3743
import java.util.Set;
44+
import java.util.concurrent.ThreadLocalRandom;
3845
import java.util.function.BooleanSupplier;
3946
import java.util.function.Supplier;
4047
import java.util.stream.Stream;
4148
import javax.annotation.Nullable;
49+
import javax.crypto.Cipher;
50+
import javax.crypto.spec.IvParameterSpec;
51+
import javax.crypto.spec.SecretKeySpec;
4252

4353
/**
4454
* Determine if JVM is impacted by https://bugs.openjdk.org/browse/JDK-8292158 which can corrupt AES-CTR encryption
@@ -100,6 +110,9 @@ static boolean isAffectedByJdkAesCtrCorruption(Version version, String architect
100110
@SuppressWarnings("checkstyle:CyclomaticComplexity")
101111
static boolean isAffectedByJdkAesCtrCorruption(
102112
Version version, String architecture, Info info, BooleanSupplier cpuHasAvx512) {
113+
if (isAesCtrBroken()) {
114+
return true;
115+
}
103116
int featureVersion = version.feature();
104117
if (featureVersion >= 20) {
105118
// https://git.openjdk.org/jdk/commit/9d76ac8a4453bc51d9dca2ad6c60259cfb2c4203 in jdk-20+17
@@ -196,4 +209,76 @@ static boolean hasVectorizedAesCpu(Stream<String> lines) {
196209
.collect(ImmutableSortedSet.toImmutableSortedSet(Comparator.naturalOrder()));
197210
return flags.containsAll(jdk8292158ImpactedCpuFlags);
198211
}
212+
213+
@VisibleForTesting
214+
static boolean isAesCtrBroken() {
215+
try {
216+
for (int i = 8; i <= 32; i++) {
217+
testEncryptDecrypt(i);
218+
}
219+
return false;
220+
} catch (NoSuchProviderException e) {
221+
log.warn("AES-CTR test failed due to no such provider", e);
222+
return false;
223+
} catch (GeneralSecurityException | Error | RuntimeException e) {
224+
log.error("AES-CTR AES-CTR encryption/decryption round-trip failed", e);
225+
return true;
226+
}
227+
}
228+
229+
static void testEncryptDecrypt(int length) throws GeneralSecurityException {
230+
Preconditions.checkArgument(length > 4, "length must be at least 4");
231+
232+
long seed = ThreadLocalRandom.current().nextLong();
233+
if (log.isDebugEnabled()) {
234+
log.debug(
235+
"Testing AES-CTR encryption/decryption for JDK-829158",
236+
SafeArg.of("seed", seed),
237+
SafeArg.of("length", length));
238+
}
239+
240+
Random random = new Random(seed);
241+
242+
byte[] key = new byte[32];
243+
random.nextBytes(key);
244+
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
245+
246+
byte[] iv = new byte[16];
247+
random.nextBytes(iv);
248+
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
249+
250+
Cipher encrypt = Cipher.getInstance("AES/CTR/NoPadding");
251+
encrypt.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
252+
253+
Cipher decrypt = Cipher.getInstance("AES/CTR/NoPadding");
254+
decrypt.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
255+
256+
byte[] cleartext = new byte[length];
257+
byte[] encrypted = new byte[length];
258+
byte[] decrypted = new byte[length];
259+
260+
for (int i = 0; i < 10_000; i++) {
261+
random.nextBytes(cleartext);
262+
encrypt.doFinal(cleartext, 0, length, encrypted);
263+
264+
// use decrypt cipher at least 3 times
265+
decrypt.update(encrypted, 0, 1, decrypted, 0);
266+
decrypt.update(encrypted, 1, 1, decrypted, 1);
267+
decrypt.doFinal(encrypted, 2, length - 2, decrypted, 2);
268+
269+
if (!Arrays.equals(cleartext, decrypted)) {
270+
throw new SafeIllegalStateException(
271+
"AES-CTR encryption/decryption round trip failed",
272+
cannotEncryptAesCtrSafely(),
273+
SafeArg.of("seed", seed),
274+
SafeArg.of("length", length),
275+
SafeArg.of("iteration", i),
276+
UnsafeArg.of("cleartext", BaseEncoding.base16().encode(cleartext)),
277+
UnsafeArg.of("decrypted", BaseEncoding.base16().encode(decrypted)),
278+
UnsafeArg.of("encrypted", BaseEncoding.base16().encode(encrypted)),
279+
UnsafeArg.of("key", BaseEncoding.base16().encode(key)),
280+
UnsafeArg.of("iv", BaseEncoding.base16().encode(iv)));
281+
}
282+
}
283+
}
199284
}

crypto-core/src/test/java/com/palantir/crypto2/cipher/Jdk8292158Test.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import static com.palantir.logsafe.testing.Assertions.assertThatLoggableExceptionThrownBy;
2020
import static org.assertj.core.api.Assertions.assertThat;
2121
import static org.assertj.core.api.Assumptions.assumeThatThrownBy;
22+
import static org.junit.jupiter.api.Assumptions.assumeFalse;
23+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
2224
import static org.mockito.Mockito.mock;
2325
import static org.mockito.Mockito.when;
2426

@@ -50,6 +52,21 @@ void aesCbcIsNotAffected() {
5052
.isFalse();
5153
}
5254

55+
@Test
56+
void throwsWhenAffected() {
57+
assumeTrue(Jdk8292158.isAesCtrBroken());
58+
assumeThatThrownBy(() -> Jdk8292158.isAffectedByJdkAesCtrCorruption(AesCtrCipher.ALGORITHM))
59+
.isInstanceOf(SafeIllegalStateException.class)
60+
.hasMessageContaining("JVM and CPU architecture is affected by JDK-8292158");
61+
}
62+
63+
@Test
64+
void doesNotThrowWhenNotAffected() {
65+
assumeFalse(Jdk8292158.isAesCtrBroken());
66+
assertThat(Jdk8292158.isAffectedByJdkAesCtrCorruption(AesCtrCipher.ALGORITHM))
67+
.isFalse();
68+
}
69+
5370
@Test
5471
void aesCtrMayBeAffected() {
5572
assumeThatThrownBy(() -> assertThat(Jdk8292158.isAffectedByJdkAesCtrCorruption(AesCtrCipher.ALGORITHM))

0 commit comments

Comments
 (0)