Browse Source

Polish gh-16164

Steve Riesenberg 6 tháng trước cách đây
mục cha
commit
5eb232cd3d

+ 24 - 5
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<CBCModeCipher> 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;
+	}
+
 }

+ 22 - 3
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<GCMBlockCipher> 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;
+	}
+
 }

+ 83 - 2
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
 	 */