From 5eb232cd3d0d09a7eac83e24a265079380f25647 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:33:00 -0600 Subject: [PATCH] Polish gh-16164 --- .../BouncyCastleAesCbcBytesEncryptor.java | 29 +++++-- .../BouncyCastleAesGcmBytesEncryptor.java | 25 +++++- ...stleAesBytesEncryptorEquivalencyTests.java | 85 ++++++++++++++++++- 3 files changed, 129 insertions(+), 10 deletions(-) diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java index 3fde93d5ea..362f28c255 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.security.crypto.encrypt; +import java.util.function.Supplier; + import org.bouncycastle.crypto.BufferedBlockCipher; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.engines.AESFastEngine; import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.modes.CBCModeCipher; import org.bouncycastle.crypto.paddings.PKCS7Padding; import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.params.ParametersWithIV; @@ -37,6 +41,8 @@ import org.springframework.security.crypto.util.EncodingUtils; */ public class BouncyCastleAesCbcBytesEncryptor extends BouncyCastleAesBytesEncryptor { + private Supplier cipherFactory = () -> CBCBlockCipher.newInstance(AESEngine.newInstance()); + public BouncyCastleAesCbcBytesEncryptor(String password, CharSequence salt) { super(password, salt); } @@ -48,8 +54,8 @@ public class BouncyCastleAesCbcBytesEncryptor extends BouncyCastleAesBytesEncryp @Override public byte[] encrypt(byte[] bytes) { byte[] iv = this.ivGenerator.generateKey(); - PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher( - CBCBlockCipher.newInstance(AESEngine.newInstance()), new PKCS7Padding()); + PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher(this.cipherFactory.get(), + new PKCS7Padding()); blockCipher.init(true, new ParametersWithIV(this.secretKey, iv)); byte[] encrypted = process(blockCipher, bytes); return (iv != null) ? EncodingUtils.concatenate(iv, encrypted) : encrypted; @@ -59,8 +65,8 @@ public class BouncyCastleAesCbcBytesEncryptor extends BouncyCastleAesBytesEncryp public byte[] decrypt(byte[] encryptedBytes) { byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, this.ivGenerator.getKeyLength()); encryptedBytes = EncodingUtils.subArray(encryptedBytes, this.ivGenerator.getKeyLength(), encryptedBytes.length); - PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher( - CBCBlockCipher.newInstance(AESEngine.newInstance()), new PKCS7Padding()); + PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher(this.cipherFactory.get(), + new PKCS7Padding()); blockCipher.init(false, new ParametersWithIV(this.secretKey, iv)); return process(blockCipher, encryptedBytes); } @@ -82,4 +88,17 @@ public class BouncyCastleAesCbcBytesEncryptor extends BouncyCastleAesBytesEncryp return out; } + /** + * Used to test compatibility with deprecated {@link AESFastEngine}. + */ + @SuppressWarnings("deprecation") + static BouncyCastleAesCbcBytesEncryptor withAESFastEngine(String password, CharSequence salt, + BytesKeyGenerator ivGenerator) { + BouncyCastleAesCbcBytesEncryptor bytesEncryptor = new BouncyCastleAesCbcBytesEncryptor(password, salt, + ivGenerator); + bytesEncryptor.cipherFactory = () -> new CBCBlockCipher(new AESFastEngine()); + + return bytesEncryptor; + } + } diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java index 20ccf9265e..52d6e6cc48 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,11 @@ package org.springframework.security.crypto.encrypt; +import java.util.function.Supplier; + import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.engines.AESFastEngine; import org.bouncycastle.crypto.modes.AEADBlockCipher; import org.bouncycastle.crypto.modes.GCMBlockCipher; import org.bouncycastle.crypto.params.AEADParameters; @@ -36,6 +39,9 @@ import org.springframework.security.crypto.util.EncodingUtils; */ public class BouncyCastleAesGcmBytesEncryptor extends BouncyCastleAesBytesEncryptor { + private Supplier cipherFactory = () -> (GCMBlockCipher) GCMBlockCipher + .newInstance(AESEngine.newInstance()); + public BouncyCastleAesGcmBytesEncryptor(String password, CharSequence salt) { super(password, salt); } @@ -47,7 +53,7 @@ public class BouncyCastleAesGcmBytesEncryptor extends BouncyCastleAesBytesEncryp @Override public byte[] encrypt(byte[] bytes) { byte[] iv = this.ivGenerator.generateKey(); - GCMBlockCipher blockCipher = (GCMBlockCipher) GCMBlockCipher.newInstance(AESEngine.newInstance()); + AEADBlockCipher blockCipher = this.cipherFactory.get(); blockCipher.init(true, new AEADParameters(this.secretKey, 128, iv, null)); byte[] encrypted = process(blockCipher, bytes); return (iv != null) ? EncodingUtils.concatenate(iv, encrypted) : encrypted; @@ -57,7 +63,7 @@ public class BouncyCastleAesGcmBytesEncryptor extends BouncyCastleAesBytesEncryp public byte[] decrypt(byte[] encryptedBytes) { byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, this.ivGenerator.getKeyLength()); encryptedBytes = EncodingUtils.subArray(encryptedBytes, this.ivGenerator.getKeyLength(), encryptedBytes.length); - GCMBlockCipher blockCipher = (GCMBlockCipher) GCMBlockCipher.newInstance(AESEngine.newInstance()); + AEADBlockCipher blockCipher = this.cipherFactory.get(); blockCipher.init(false, new AEADParameters(this.secretKey, 128, iv, null)); return process(blockCipher, encryptedBytes); } @@ -79,4 +85,17 @@ public class BouncyCastleAesGcmBytesEncryptor extends BouncyCastleAesBytesEncryp return out; } + /** + * Used to test compatibility with deprecated {@link AESFastEngine}. + */ + @SuppressWarnings("deprecation") + static BouncyCastleAesGcmBytesEncryptor withAESFastEngine(String password, CharSequence salt, + BytesKeyGenerator ivGenerator) { + BouncyCastleAesGcmBytesEncryptor bytesEncryptor = new BouncyCastleAesGcmBytesEncryptor(password, salt, + ivGenerator); + bytesEncryptor.cipherFactory = () -> new GCMBlockCipher(new AESFastEngine()); + + return bytesEncryptor; + } + } diff --git a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java index a37c83092c..0dda6ebfc5 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2021 the original author or authors. + * Copyright 2011-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,14 @@ package org.springframework.security.crypto.encrypt; import java.security.SecureRandom; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Random; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.codec.Hex; @@ -89,6 +93,64 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests { testCompatibility(bcEncryptor, jceEncryptor); } + @Test + public void bouncyCastleAesGcmWithAESFastEngineCompatible() throws Exception { + CryptoAssumptions.assumeGCMJCE(); + BytesEncryptor fastEngineEncryptor = BouncyCastleAesGcmBytesEncryptor.withAESFastEngine(this.password, + this.salt, KeyGenerators.secureRandom(16)); + BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesGcmBytesEncryptor(this.password, this.salt, + KeyGenerators.secureRandom(16)); + testCompatibility(fastEngineEncryptor, defaultEngineEncryptor); + } + + @Test + public void bouncyCastleAesCbcWithAESFastEngineCompatible() throws Exception { + CryptoAssumptions.assumeCBCJCE(); + BytesEncryptor fastEngineEncryptor = BouncyCastleAesCbcBytesEncryptor.withAESFastEngine(this.password, + this.salt, KeyGenerators.secureRandom(16)); + BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesCbcBytesEncryptor(this.password, this.salt, + KeyGenerators.secureRandom(16)); + testCompatibility(fastEngineEncryptor, defaultEngineEncryptor); + } + + /** + * Comment out @Disabled below to compare relative speed of deprecated AESFastEngine + * with the default AESEngine. + */ + @Disabled + @RepeatedTest(100) + public void bouncyCastleAesGcmWithAESFastEngineSpeedTest() throws Exception { + CryptoAssumptions.assumeGCMJCE(); + BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesGcmBytesEncryptor(this.password, this.salt, + KeyGenerators.secureRandom(16)); + BytesEncryptor fastEngineEncryptor = BouncyCastleAesGcmBytesEncryptor.withAESFastEngine(this.password, + this.salt, KeyGenerators.secureRandom(16)); + long defaultNanos = testSpeed(defaultEngineEncryptor); + long fastNanos = testSpeed(fastEngineEncryptor); + System.out.println(nanosToReadableString("AES GCM w/Default Engine", defaultNanos)); + System.out.println(nanosToReadableString("AES GCM w/ Fast Engine", fastNanos)); + assertThat(fastNanos).isLessThan(defaultNanos); + } + + /** + * Comment out @Disabled below to compare relative speed of deprecated AESFastEngine + * with the default AESEngine. + */ + @Disabled + @RepeatedTest(100) + public void bouncyCastleAesCbcWithAESFastEngineSpeedTest() throws Exception { + CryptoAssumptions.assumeCBCJCE(); + BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesCbcBytesEncryptor(this.password, this.salt, + KeyGenerators.secureRandom(16)); + BytesEncryptor fastEngineEncryptor = BouncyCastleAesCbcBytesEncryptor.withAESFastEngine(this.password, + this.salt, KeyGenerators.secureRandom(16)); + long defaultNanos = testSpeed(defaultEngineEncryptor); + long fastNanos = testSpeed(fastEngineEncryptor); + System.out.println(nanosToReadableString("AES CBC w/Default Engine", defaultNanos)); + System.out.println(nanosToReadableString("AES CBC w/ Fast Engine", fastNanos)); + assertThat(fastNanos).isLessThan(defaultNanos); + } + private void testEquivalence(BytesEncryptor left, BytesEncryptor right) { for (int size = 1; size < 2048; size++) { this.testData = new byte[size]; @@ -107,7 +169,7 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests { private void testCompatibility(BytesEncryptor left, BytesEncryptor right) { // tests that right can decrypt what left encrypted and vice versa - // and that the decypted data is the same as the original + // and that the decrypted data is the same as the original for (int size = 1; size < 2048; size++) { this.testData = new byte[size]; this.secureRandom.nextBytes(this.testData); @@ -120,6 +182,25 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests { } } + private long testSpeed(BytesEncryptor bytesEncryptor) { + long start = System.nanoTime(); + for (int size = 0; size < 2048; size++) { + this.testData = new byte[size]; + this.secureRandom.nextBytes(this.testData); + byte[] encrypted = bytesEncryptor.encrypt(this.testData); + byte[] decrypted = bytesEncryptor.decrypt(encrypted); + assertThat(decrypted).containsExactly(this.testData); + } + return System.nanoTime() - start; + } + + private String nanosToReadableString(String label, long nanos) { + Duration duration = Duration.ofNanos(nanos); + Duration millis = duration.truncatedTo(ChronoUnit.MILLIS); + Duration micros = duration.minus(millis).dividedBy(1000); + return "%s: %dms %dμs".formatted(label, duration.toMillis(), micros.toNanos()); + } + /** * A BytesKeyGenerator that always generates the same sequence of values */