Skip to content

Commit 5eb232c

Browse files
sjohnrrwinch
authored andcommitted
Polish gh-16164
1 parent 2b22cf2 commit 5eb232c

File tree

3 files changed

+129
-10
lines changed

3 files changed

+129
-10
lines changed

crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesCbcBytesEncryptor.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2016 the original author or authors.
2+
* Copyright 2011-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,14 @@
1616

1717
package org.springframework.security.crypto.encrypt;
1818

19+
import java.util.function.Supplier;
20+
1921
import org.bouncycastle.crypto.BufferedBlockCipher;
2022
import org.bouncycastle.crypto.InvalidCipherTextException;
2123
import org.bouncycastle.crypto.engines.AESEngine;
24+
import org.bouncycastle.crypto.engines.AESFastEngine;
2225
import org.bouncycastle.crypto.modes.CBCBlockCipher;
26+
import org.bouncycastle.crypto.modes.CBCModeCipher;
2327
import org.bouncycastle.crypto.paddings.PKCS7Padding;
2428
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
2529
import org.bouncycastle.crypto.params.ParametersWithIV;
@@ -37,6 +41,8 @@
3741
*/
3842
public class BouncyCastleAesCbcBytesEncryptor extends BouncyCastleAesBytesEncryptor {
3943

44+
private Supplier<CBCModeCipher> cipherFactory = () -> CBCBlockCipher.newInstance(AESEngine.newInstance());
45+
4046
public BouncyCastleAesCbcBytesEncryptor(String password, CharSequence salt) {
4147
super(password, salt);
4248
}
@@ -48,8 +54,8 @@ public BouncyCastleAesCbcBytesEncryptor(String password, CharSequence salt, Byte
4854
@Override
4955
public byte[] encrypt(byte[] bytes) {
5056
byte[] iv = this.ivGenerator.generateKey();
51-
PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher(
52-
CBCBlockCipher.newInstance(AESEngine.newInstance()), new PKCS7Padding());
57+
PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher(this.cipherFactory.get(),
58+
new PKCS7Padding());
5359
blockCipher.init(true, new ParametersWithIV(this.secretKey, iv));
5460
byte[] encrypted = process(blockCipher, bytes);
5561
return (iv != null) ? EncodingUtils.concatenate(iv, encrypted) : encrypted;
@@ -59,8 +65,8 @@ public byte[] encrypt(byte[] bytes) {
5965
public byte[] decrypt(byte[] encryptedBytes) {
6066
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, this.ivGenerator.getKeyLength());
6167
encryptedBytes = EncodingUtils.subArray(encryptedBytes, this.ivGenerator.getKeyLength(), encryptedBytes.length);
62-
PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher(
63-
CBCBlockCipher.newInstance(AESEngine.newInstance()), new PKCS7Padding());
68+
PaddedBufferedBlockCipher blockCipher = new PaddedBufferedBlockCipher(this.cipherFactory.get(),
69+
new PKCS7Padding());
6470
blockCipher.init(false, new ParametersWithIV(this.secretKey, iv));
6571
return process(blockCipher, encryptedBytes);
6672
}
@@ -82,4 +88,17 @@ private byte[] process(BufferedBlockCipher blockCipher, byte[] in) {
8288
return out;
8389
}
8490

91+
/**
92+
* Used to test compatibility with deprecated {@link AESFastEngine}.
93+
*/
94+
@SuppressWarnings("deprecation")
95+
static BouncyCastleAesCbcBytesEncryptor withAESFastEngine(String password, CharSequence salt,
96+
BytesKeyGenerator ivGenerator) {
97+
BouncyCastleAesCbcBytesEncryptor bytesEncryptor = new BouncyCastleAesCbcBytesEncryptor(password, salt,
98+
ivGenerator);
99+
bytesEncryptor.cipherFactory = () -> new CBCBlockCipher(new AESFastEngine());
100+
101+
return bytesEncryptor;
102+
}
103+
85104
}

crypto/src/main/java/org/springframework/security/crypto/encrypt/BouncyCastleAesGcmBytesEncryptor.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2016 the original author or authors.
2+
* Copyright 2011-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,11 @@
1616

1717
package org.springframework.security.crypto.encrypt;
1818

19+
import java.util.function.Supplier;
20+
1921
import org.bouncycastle.crypto.InvalidCipherTextException;
2022
import org.bouncycastle.crypto.engines.AESEngine;
23+
import org.bouncycastle.crypto.engines.AESFastEngine;
2124
import org.bouncycastle.crypto.modes.AEADBlockCipher;
2225
import org.bouncycastle.crypto.modes.GCMBlockCipher;
2326
import org.bouncycastle.crypto.params.AEADParameters;
@@ -36,6 +39,9 @@
3639
*/
3740
public class BouncyCastleAesGcmBytesEncryptor extends BouncyCastleAesBytesEncryptor {
3841

42+
private Supplier<GCMBlockCipher> cipherFactory = () -> (GCMBlockCipher) GCMBlockCipher
43+
.newInstance(AESEngine.newInstance());
44+
3945
public BouncyCastleAesGcmBytesEncryptor(String password, CharSequence salt) {
4046
super(password, salt);
4147
}
@@ -47,7 +53,7 @@ public BouncyCastleAesGcmBytesEncryptor(String password, CharSequence salt, Byte
4753
@Override
4854
public byte[] encrypt(byte[] bytes) {
4955
byte[] iv = this.ivGenerator.generateKey();
50-
GCMBlockCipher blockCipher = (GCMBlockCipher) GCMBlockCipher.newInstance(AESEngine.newInstance());
56+
AEADBlockCipher blockCipher = this.cipherFactory.get();
5157
blockCipher.init(true, new AEADParameters(this.secretKey, 128, iv, null));
5258
byte[] encrypted = process(blockCipher, bytes);
5359
return (iv != null) ? EncodingUtils.concatenate(iv, encrypted) : encrypted;
@@ -57,7 +63,7 @@ public byte[] encrypt(byte[] bytes) {
5763
public byte[] decrypt(byte[] encryptedBytes) {
5864
byte[] iv = EncodingUtils.subArray(encryptedBytes, 0, this.ivGenerator.getKeyLength());
5965
encryptedBytes = EncodingUtils.subArray(encryptedBytes, this.ivGenerator.getKeyLength(), encryptedBytes.length);
60-
GCMBlockCipher blockCipher = (GCMBlockCipher) GCMBlockCipher.newInstance(AESEngine.newInstance());
66+
AEADBlockCipher blockCipher = this.cipherFactory.get();
6167
blockCipher.init(false, new AEADParameters(this.secretKey, 128, iv, null));
6268
return process(blockCipher, encryptedBytes);
6369
}
@@ -79,4 +85,17 @@ private byte[] process(AEADBlockCipher blockCipher, byte[] in) {
7985
return out;
8086
}
8187

88+
/**
89+
* Used to test compatibility with deprecated {@link AESFastEngine}.
90+
*/
91+
@SuppressWarnings("deprecation")
92+
static BouncyCastleAesGcmBytesEncryptor withAESFastEngine(String password, CharSequence salt,
93+
BytesKeyGenerator ivGenerator) {
94+
BouncyCastleAesGcmBytesEncryptor bytesEncryptor = new BouncyCastleAesGcmBytesEncryptor(password, salt,
95+
ivGenerator);
96+
bytesEncryptor.cipherFactory = () -> new GCMBlockCipher(new AESFastEngine());
97+
98+
return bytesEncryptor;
99+
}
100+
82101
}

crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2021 the original author or authors.
2+
* Copyright 2011-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,10 +17,14 @@
1717
package org.springframework.security.crypto.encrypt;
1818

1919
import java.security.SecureRandom;
20+
import java.time.Duration;
21+
import java.time.temporal.ChronoUnit;
2022
import java.util.Random;
2123
import java.util.UUID;
2224

2325
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Disabled;
27+
import org.junit.jupiter.api.RepeatedTest;
2428
import org.junit.jupiter.api.Test;
2529

2630
import org.springframework.security.crypto.codec.Hex;
@@ -89,6 +93,64 @@ public void bouncyCastleAesGcmWithSecureIvCompatible() throws Exception {
8993
testCompatibility(bcEncryptor, jceEncryptor);
9094
}
9195

96+
@Test
97+
public void bouncyCastleAesGcmWithAESFastEngineCompatible() throws Exception {
98+
CryptoAssumptions.assumeGCMJCE();
99+
BytesEncryptor fastEngineEncryptor = BouncyCastleAesGcmBytesEncryptor.withAESFastEngine(this.password,
100+
this.salt, KeyGenerators.secureRandom(16));
101+
BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesGcmBytesEncryptor(this.password, this.salt,
102+
KeyGenerators.secureRandom(16));
103+
testCompatibility(fastEngineEncryptor, defaultEngineEncryptor);
104+
}
105+
106+
@Test
107+
public void bouncyCastleAesCbcWithAESFastEngineCompatible() throws Exception {
108+
CryptoAssumptions.assumeCBCJCE();
109+
BytesEncryptor fastEngineEncryptor = BouncyCastleAesCbcBytesEncryptor.withAESFastEngine(this.password,
110+
this.salt, KeyGenerators.secureRandom(16));
111+
BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesCbcBytesEncryptor(this.password, this.salt,
112+
KeyGenerators.secureRandom(16));
113+
testCompatibility(fastEngineEncryptor, defaultEngineEncryptor);
114+
}
115+
116+
/**
117+
* Comment out @Disabled below to compare relative speed of deprecated AESFastEngine
118+
* with the default AESEngine.
119+
*/
120+
@Disabled
121+
@RepeatedTest(100)
122+
public void bouncyCastleAesGcmWithAESFastEngineSpeedTest() throws Exception {
123+
CryptoAssumptions.assumeGCMJCE();
124+
BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesGcmBytesEncryptor(this.password, this.salt,
125+
KeyGenerators.secureRandom(16));
126+
BytesEncryptor fastEngineEncryptor = BouncyCastleAesGcmBytesEncryptor.withAESFastEngine(this.password,
127+
this.salt, KeyGenerators.secureRandom(16));
128+
long defaultNanos = testSpeed(defaultEngineEncryptor);
129+
long fastNanos = testSpeed(fastEngineEncryptor);
130+
System.out.println(nanosToReadableString("AES GCM w/Default Engine", defaultNanos));
131+
System.out.println(nanosToReadableString("AES GCM w/ Fast Engine", fastNanos));
132+
assertThat(fastNanos).isLessThan(defaultNanos);
133+
}
134+
135+
/**
136+
* Comment out @Disabled below to compare relative speed of deprecated AESFastEngine
137+
* with the default AESEngine.
138+
*/
139+
@Disabled
140+
@RepeatedTest(100)
141+
public void bouncyCastleAesCbcWithAESFastEngineSpeedTest() throws Exception {
142+
CryptoAssumptions.assumeCBCJCE();
143+
BytesEncryptor defaultEngineEncryptor = new BouncyCastleAesCbcBytesEncryptor(this.password, this.salt,
144+
KeyGenerators.secureRandom(16));
145+
BytesEncryptor fastEngineEncryptor = BouncyCastleAesCbcBytesEncryptor.withAESFastEngine(this.password,
146+
this.salt, KeyGenerators.secureRandom(16));
147+
long defaultNanos = testSpeed(defaultEngineEncryptor);
148+
long fastNanos = testSpeed(fastEngineEncryptor);
149+
System.out.println(nanosToReadableString("AES CBC w/Default Engine", defaultNanos));
150+
System.out.println(nanosToReadableString("AES CBC w/ Fast Engine", fastNanos));
151+
assertThat(fastNanos).isLessThan(defaultNanos);
152+
}
153+
92154
private void testEquivalence(BytesEncryptor left, BytesEncryptor right) {
93155
for (int size = 1; size < 2048; size++) {
94156
this.testData = new byte[size];
@@ -107,7 +169,7 @@ private void testEquivalence(BytesEncryptor left, BytesEncryptor right) {
107169

108170
private void testCompatibility(BytesEncryptor left, BytesEncryptor right) {
109171
// tests that right can decrypt what left encrypted and vice versa
110-
// and that the decypted data is the same as the original
172+
// and that the decrypted data is the same as the original
111173
for (int size = 1; size < 2048; size++) {
112174
this.testData = new byte[size];
113175
this.secureRandom.nextBytes(this.testData);
@@ -120,6 +182,25 @@ private void testCompatibility(BytesEncryptor left, BytesEncryptor right) {
120182
}
121183
}
122184

185+
private long testSpeed(BytesEncryptor bytesEncryptor) {
186+
long start = System.nanoTime();
187+
for (int size = 0; size < 2048; size++) {
188+
this.testData = new byte[size];
189+
this.secureRandom.nextBytes(this.testData);
190+
byte[] encrypted = bytesEncryptor.encrypt(this.testData);
191+
byte[] decrypted = bytesEncryptor.decrypt(encrypted);
192+
assertThat(decrypted).containsExactly(this.testData);
193+
}
194+
return System.nanoTime() - start;
195+
}
196+
197+
private String nanosToReadableString(String label, long nanos) {
198+
Duration duration = Duration.ofNanos(nanos);
199+
Duration millis = duration.truncatedTo(ChronoUnit.MILLIS);
200+
Duration micros = duration.minus(millis).dividedBy(1000);
201+
return "%s: %dms %dμs".formatted(label, duration.toMillis(), micros.toNanos());
202+
}
203+
123204
/**
124205
* A BytesKeyGenerator that always generates the same sequence of values
125206
*/

0 commit comments

Comments
 (0)