Prechádzať zdrojové kódy

Migrate spring-security-rsa into spring-security-crypto

Closes gh-14202
Marcus Hert Da Coregio 1 rok pred
rodič
commit
6f7b9bbfde

+ 2 - 1
crypto/spring-security-crypto.gradle

@@ -3,8 +3,9 @@ apply plugin: 'io.spring.convention.spring-module'
 dependencies {
 	management platform(project(":spring-security-dependencies"))
 	optional 'org.springframework:spring-jcl'
+	optional 'org.springframework:spring-core'
 	optional 'org.bouncycastle:bcpkix-jdk15on'
-	
+
 	testImplementation "org.assertj:assertj-core"
 	testImplementation "org.junit.jupiter:junit-jupiter-api"
 	testImplementation "org.junit.jupiter:junit-jupiter-params"

+ 96 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/KeyStoreKeyFactory.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import java.io.InputStream;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.spec.RSAPublicKeySpec;
+
+import org.springframework.core.io.Resource;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Dave Syer
+ * @author Tim Ysewyn
+ * @since 6.3
+ */
+public class KeyStoreKeyFactory {
+
+	private final Resource resource;
+
+	private final char[] password;
+
+	private KeyStore store;
+
+	private final Object lock = new Object();
+
+	private final String type;
+
+	public KeyStoreKeyFactory(Resource resource, char[] password) {
+		this(resource, password, type(resource));
+	}
+
+	private static String type(Resource resource) {
+		String ext = StringUtils.getFilenameExtension(resource.getFilename());
+		return (ext != null) ? ext : "jks";
+	}
+
+	public KeyStoreKeyFactory(Resource resource, char[] password, String type) {
+		this.resource = resource;
+		this.password = password;
+		this.type = type;
+	}
+
+	public KeyPair getKeyPair(String alias) {
+		return getKeyPair(alias, this.password);
+	}
+
+	public KeyPair getKeyPair(String alias, char[] password) {
+		try {
+			synchronized (this.lock) {
+				if (this.store == null) {
+					synchronized (this.lock) {
+						this.store = KeyStore.getInstance(this.type);
+						try (InputStream stream = this.resource.getInputStream()) {
+							this.store.load(stream, this.password);
+						}
+					}
+				}
+			}
+			RSAPrivateCrtKey key = (RSAPrivateCrtKey) this.store.getKey(alias, password);
+			Certificate certificate = this.store.getCertificate(alias);
+			PublicKey publicKey = null;
+			if (certificate != null) {
+				publicKey = certificate.getPublicKey();
+			}
+			else if (key != null) {
+				RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
+				publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
+			}
+			return new KeyPair(publicKey, key);
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException("Cannot load keys from store: " + this.resource, ex);
+		}
+	}
+
+}

+ 44 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaAlgorithm.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+/**
+ * @author Dave Syer
+ * @since 6.3
+ */
+public enum RsaAlgorithm {
+
+	DEFAULT("RSA", 117), OAEP("RSA/ECB/OAEPPadding", 86);
+
+	private final String name;
+
+	private final int maxLength;
+
+	RsaAlgorithm(String name, int maxLength) {
+		this.name = name;
+		this.maxLength = maxLength;
+	}
+
+	public String getJceName() {
+		return this.name;
+	}
+
+	public int getMaxLength() {
+		return this.maxLength;
+	}
+
+}

+ 284 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaKeyHelper.java

@@ -0,0 +1,284 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.RSAPrivateCrtKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.bouncycastle.asn1.ASN1Sequence;
+
+/**
+ * Reads RSA key pairs using BC provider classes but without the need to specify a crypto
+ * provider or have BC added as one.
+ *
+ * @author Luke Taylor
+ * @author Dave Syer
+ */
+final class RsaKeyHelper {
+
+	private static final Charset UTF8 = StandardCharsets.UTF_8;
+
+	private static final String BEGIN = "-----BEGIN";
+
+	private static final Pattern PEM_DATA = Pattern.compile(".*-----BEGIN (.*)-----(.*)-----END (.*)-----",
+			Pattern.DOTALL);
+
+	private static final byte[] PREFIX = new byte[] { 0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a' };
+
+	private RsaKeyHelper() {
+	}
+
+	static KeyPair parseKeyPair(String pemData) {
+		Matcher m = PEM_DATA.matcher(pemData.replaceAll("\n *", "").trim());
+
+		if (!m.matches()) {
+			try {
+				RSAPublicKey publicValue = extractPublicKey(pemData);
+				if (publicValue != null) {
+					return new KeyPair(publicValue, null);
+				}
+			}
+			catch (Exception ex) {
+				// Ignore
+			}
+			throw new IllegalArgumentException("String is not PEM encoded data, nor a public key encoded for ssh");
+		}
+
+		String type = m.group(1);
+		final byte[] content = base64Decode(m.group(2));
+
+		PublicKey publicKey;
+		PrivateKey privateKey = null;
+
+		try {
+			KeyFactory fact = KeyFactory.getInstance("RSA");
+			switch (type) {
+				case "RSA PRIVATE KEY" -> {
+					ASN1Sequence seq = ASN1Sequence.getInstance(content);
+					if (seq.size() != 9) {
+						throw new IllegalArgumentException("Invalid RSA Private Key ASN1 sequence.");
+					}
+					org.bouncycastle.asn1.pkcs.RSAPrivateKey key = org.bouncycastle.asn1.pkcs.RSAPrivateKey
+						.getInstance(seq);
+					RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
+					RSAPrivateCrtKeySpec privSpec = new RSAPrivateCrtKeySpec(key.getModulus(), key.getPublicExponent(),
+							key.getPrivateExponent(), key.getPrime1(), key.getPrime2(), key.getExponent1(),
+							key.getExponent2(), key.getCoefficient());
+					publicKey = fact.generatePublic(pubSpec);
+					privateKey = fact.generatePrivate(privSpec);
+				}
+				case "PUBLIC KEY" -> {
+					KeySpec keySpec = new X509EncodedKeySpec(content);
+					publicKey = fact.generatePublic(keySpec);
+				}
+				case "RSA PUBLIC KEY" -> {
+					ASN1Sequence seq = ASN1Sequence.getInstance(content);
+					org.bouncycastle.asn1.pkcs.RSAPublicKey key = org.bouncycastle.asn1.pkcs.RSAPublicKey
+						.getInstance(seq);
+					RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
+					publicKey = fact.generatePublic(pubSpec);
+				}
+				default -> throw new IllegalArgumentException(type + " is not a supported format");
+			}
+
+			return new KeyPair(publicKey, privateKey);
+		}
+		catch (InvalidKeySpecException ex) {
+			throw new RuntimeException(ex);
+		}
+		catch (NoSuchAlgorithmException ex) {
+			throw new IllegalStateException(ex);
+		}
+	}
+
+	private static byte[] base64Decode(String string) {
+		try {
+			ByteBuffer bytes = UTF8.newEncoder().encode(CharBuffer.wrap(string));
+			byte[] bytesCopy = new byte[bytes.limit()];
+			System.arraycopy(bytes.array(), 0, bytesCopy, 0, bytes.limit());
+			return Base64.getDecoder().decode(bytesCopy);
+		}
+		catch (CharacterCodingException ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	static String base64Encode(byte[] bytes) {
+		try {
+			return UTF8.newDecoder().decode(ByteBuffer.wrap(Base64.getEncoder().encode(bytes))).toString();
+		}
+		catch (CharacterCodingException ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	static KeyPair generateKeyPair() {
+		try {
+			final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+			keyGen.initialize(1024);
+			return keyGen.generateKeyPair();
+		}
+		catch (NoSuchAlgorithmException ex) {
+			throw new IllegalStateException(ex);
+		}
+
+	}
+
+	private static final Pattern SSH_PUB_KEY = Pattern.compile("ssh-(rsa|dsa) ([A-Za-z0-9/+]+=*) (.*)");
+
+	private static RSAPublicKey extractPublicKey(String key) {
+
+		Matcher m = SSH_PUB_KEY.matcher(key);
+
+		if (m.matches()) {
+			String alg = m.group(1);
+			String encKey = m.group(2);
+			// String id = m.group(3);
+
+			if (!"rsa".equalsIgnoreCase(alg)) {
+				throw new IllegalArgumentException("Only RSA is currently supported, but algorithm was " + alg);
+			}
+
+			return parseSSHPublicKey(encKey);
+		}
+		else if (!key.startsWith(BEGIN)) {
+			// Assume it's the plain Base64 encoded ssh key without the
+			// "ssh-rsa" at the start
+			return parseSSHPublicKey(key);
+		}
+
+		return null;
+	}
+
+	static RSAPublicKey parsePublicKey(String key) {
+
+		RSAPublicKey publicKey = extractPublicKey(key);
+
+		if (publicKey != null) {
+			return publicKey;
+		}
+
+		KeyPair kp = parseKeyPair(key);
+
+		if (kp.getPublic() == null) {
+			throw new IllegalArgumentException("Key data does not contain a public key");
+		}
+
+		return (RSAPublicKey) kp.getPublic();
+
+	}
+
+	static String encodePublicKey(RSAPublicKey key, String id) {
+		StringWriter output = new StringWriter();
+		output.append("ssh-rsa ");
+		ByteArrayOutputStream stream = new ByteArrayOutputStream();
+		try {
+			stream.write(PREFIX);
+			writeBigInteger(stream, key.getPublicExponent());
+			writeBigInteger(stream, key.getModulus());
+		}
+		catch (IOException ex) {
+			throw new IllegalStateException("Cannot encode key", ex);
+		}
+		output.append(base64Encode(stream.toByteArray()));
+		output.append(" " + id);
+		return output.toString();
+	}
+
+	private static RSAPublicKey parseSSHPublicKey(String encKey) {
+		ByteArrayInputStream in = new ByteArrayInputStream(base64Decode(encKey));
+
+		byte[] prefix = new byte[11];
+
+		try {
+			if (in.read(prefix) != 11 || !Arrays.equals(PREFIX, prefix)) {
+				throw new IllegalArgumentException("SSH key prefix not found");
+			}
+
+			BigInteger e = new BigInteger(readBigInteger(in));
+			BigInteger n = new BigInteger(readBigInteger(in));
+
+			return createPublicKey(n, e);
+		}
+		catch (IOException ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	static RSAPublicKey createPublicKey(BigInteger n, BigInteger e) {
+		try {
+			return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(n, e));
+		}
+		catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	private static void writeBigInteger(ByteArrayOutputStream stream, BigInteger num) throws IOException {
+		int length = num.toByteArray().length;
+		byte[] data = new byte[4];
+		data[0] = (byte) ((length >> 24) & 0xFF);
+		data[1] = (byte) ((length >> 16) & 0xFF);
+		data[2] = (byte) ((length >> 8) & 0xFF);
+		data[3] = (byte) (length & 0xFF);
+		stream.write(data);
+		stream.write(num.toByteArray());
+	}
+
+	private static byte[] readBigInteger(ByteArrayInputStream in) throws IOException {
+		byte[] b = new byte[4];
+
+		if (in.read(b) != 4) {
+			throw new IOException("Expected length data as 4 bytes");
+		}
+
+		int l = ((b[0] & 0xFF) << 24) | ((b[1] & 0xFF) << 16) | ((b[2] & 0xFF) << 8) | (b[3] & 0xFF);
+
+		b = new byte[l];
+
+		if (in.read(b) != l) {
+			throw new IOException("Expected " + l + " key bytes");
+		}
+
+		return b;
+	}
+
+}

+ 27 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaKeyHolder.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+/**
+ * @author Dave Syer
+ * @since 6.3
+ */
+public interface RsaKeyHolder {
+
+	String getPublicKey();
+
+}

+ 168 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaRawEncryptor.java

@@ -0,0 +1,168 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.RSAKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+
+/**
+ * @author Dave Syer
+ * @since 6.3
+ */
+public class RsaRawEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHolder {
+
+	private static final String DEFAULT_ENCODING = "UTF-8";
+
+	private RsaAlgorithm algorithm = RsaAlgorithm.DEFAULT;
+
+	private Charset charset;
+
+	private RSAPublicKey publicKey;
+
+	private RSAPrivateKey privateKey;
+
+	private Charset defaultCharset;
+
+	public RsaRawEncryptor(RsaAlgorithm algorithm) {
+		this(RsaKeyHelper.generateKeyPair(), algorithm);
+	}
+
+	public RsaRawEncryptor() {
+		this(RsaKeyHelper.generateKeyPair());
+	}
+
+	public RsaRawEncryptor(KeyPair keyPair, RsaAlgorithm algorithm) {
+		this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm);
+	}
+
+	public RsaRawEncryptor(KeyPair keyPair) {
+		this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate());
+	}
+
+	public RsaRawEncryptor(String pemData) {
+		this(RsaKeyHelper.parseKeyPair(pemData));
+	}
+
+	public RsaRawEncryptor(PublicKey publicKey) {
+		this(DEFAULT_ENCODING, publicKey, null);
+	}
+
+	public RsaRawEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey) {
+		this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
+	}
+
+	public RsaRawEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm) {
+		this.charset = Charset.forName(encoding);
+		this.publicKey = (RSAPublicKey) publicKey;
+		this.privateKey = (RSAPrivateKey) privateKey;
+		this.defaultCharset = Charset.forName(DEFAULT_ENCODING);
+		this.algorithm = algorithm;
+	}
+
+	@Override
+	public String getPublicKey() {
+		return RsaKeyHelper.encodePublicKey(this.publicKey, "application");
+	}
+
+	@Override
+	public String encrypt(String text) {
+		return new String(Base64.getEncoder().encode(encrypt(text.getBytes(this.charset))), this.defaultCharset);
+	}
+
+	@Override
+	public String decrypt(String encryptedText) {
+		if (this.privateKey == null) {
+			throw new IllegalStateException("Private key must be provided for decryption");
+		}
+		return new String(decrypt(Base64.getDecoder().decode(encryptedText.getBytes(this.defaultCharset))),
+				this.charset);
+	}
+
+	@Override
+	public byte[] encrypt(byte[] byteArray) {
+		return encrypt(byteArray, this.publicKey, this.algorithm);
+	}
+
+	@Override
+	public byte[] decrypt(byte[] encryptedByteArray) {
+		return decrypt(encryptedByteArray, this.privateKey, this.algorithm);
+	}
+
+	private static byte[] encrypt(byte[] text, PublicKey key, RsaAlgorithm alg) {
+		ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
+		try {
+			final Cipher cipher = Cipher.getInstance(alg.getJceName());
+			int limit = Math.min(text.length, alg.getMaxLength());
+			int pos = 0;
+			while (pos < text.length) {
+				cipher.init(Cipher.ENCRYPT_MODE, key);
+				cipher.update(text, pos, limit);
+				pos += limit;
+				limit = Math.min(text.length - pos, alg.getMaxLength());
+				byte[] buffer = cipher.doFinal();
+				output.write(buffer, 0, buffer.length);
+			}
+			return output.toByteArray();
+		}
+		catch (RuntimeException ex) {
+			throw ex;
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException("Cannot encrypt", ex);
+		}
+	}
+
+	private static byte[] decrypt(byte[] text, RSAPrivateKey key, RsaAlgorithm alg) {
+		ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
+		try {
+			final Cipher cipher = Cipher.getInstance(alg.getJceName());
+			int maxLength = getByteLength(key);
+			int pos = 0;
+			while (pos < text.length) {
+				int limit = Math.min(text.length - pos, maxLength);
+				cipher.init(Cipher.DECRYPT_MODE, key);
+				cipher.update(text, pos, limit);
+				pos += limit;
+				byte[] buffer = cipher.doFinal();
+				output.write(buffer, 0, buffer.length);
+			}
+			return output.toByteArray();
+		}
+		catch (RuntimeException ex) {
+			throw ex;
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException("Cannot decrypt", ex);
+		}
+	}
+
+	// copied from sun.security.rsa.RSACore.getByteLength(java.math.BigInteger)
+	public static int getByteLength(RSAKey key) {
+		int n = key.getModulus().bitLength();
+		return (n + 7) >> 3;
+	}
+
+}

+ 247 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaSecretEncryptor.java

@@ -0,0 +1,247 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+
+import org.springframework.security.crypto.codec.Hex;
+import org.springframework.security.crypto.keygen.KeyGenerators;
+
+/**
+ * @author Dave Syer
+ * @since 6.3
+ */
+public class RsaSecretEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHolder {
+
+	private static final String DEFAULT_ENCODING = "UTF-8";
+
+	// The secret for encryption is random (so dictionary attack is not a danger)
+	private static final String DEFAULT_SALT = "deadbeef";
+
+	private final String salt;
+
+	private RsaAlgorithm algorithm = RsaAlgorithm.DEFAULT;
+
+	private final Charset charset;
+
+	private final PublicKey publicKey;
+
+	private final PrivateKey privateKey;
+
+	private final Charset defaultCharset;
+
+	private final boolean gcm;
+
+	public RsaSecretEncryptor(RsaAlgorithm algorithm, String salt, boolean gcm) {
+		this(RsaKeyHelper.generateKeyPair(), algorithm, salt, gcm);
+	}
+
+	public RsaSecretEncryptor(RsaAlgorithm algorithm, String salt) {
+		this(RsaKeyHelper.generateKeyPair(), algorithm, salt);
+	}
+
+	public RsaSecretEncryptor(RsaAlgorithm algorithm, boolean gcm) {
+		this(RsaKeyHelper.generateKeyPair(), algorithm, DEFAULT_SALT, gcm);
+	}
+
+	public RsaSecretEncryptor(RsaAlgorithm algorithm) {
+		this(RsaKeyHelper.generateKeyPair(), algorithm);
+	}
+
+	public RsaSecretEncryptor() {
+		this(RsaKeyHelper.generateKeyPair());
+	}
+
+	public RsaSecretEncryptor(KeyPair keyPair, RsaAlgorithm algorithm, String salt, boolean gcm) {
+		this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm, salt, gcm);
+	}
+
+	public RsaSecretEncryptor(KeyPair keyPair, RsaAlgorithm algorithm, String salt) {
+		this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm, salt, false);
+	}
+
+	public RsaSecretEncryptor(KeyPair keyPair, RsaAlgorithm algorithm) {
+		this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate(), algorithm);
+	}
+
+	public RsaSecretEncryptor(KeyPair keyPair) {
+		this(DEFAULT_ENCODING, keyPair.getPublic(), keyPair.getPrivate());
+	}
+
+	public RsaSecretEncryptor(String pemData, RsaAlgorithm algorithm, String salt) {
+		this(RsaKeyHelper.parseKeyPair(pemData), algorithm, salt);
+	}
+
+	public RsaSecretEncryptor(String pemData, RsaAlgorithm algorithm) {
+		this(RsaKeyHelper.parseKeyPair(pemData), algorithm);
+	}
+
+	public RsaSecretEncryptor(String pemData) {
+		this(RsaKeyHelper.parseKeyPair(pemData));
+	}
+
+	public RsaSecretEncryptor(PublicKey publicKey, RsaAlgorithm algorithm, String salt, boolean gcm) {
+		this(DEFAULT_ENCODING, publicKey, null, algorithm, salt, gcm);
+	}
+
+	public RsaSecretEncryptor(PublicKey publicKey, RsaAlgorithm algorithm, String salt) {
+		this(DEFAULT_ENCODING, publicKey, null, algorithm, salt, false);
+	}
+
+	public RsaSecretEncryptor(PublicKey publicKey, RsaAlgorithm algorithm) {
+		this(DEFAULT_ENCODING, publicKey, null, algorithm);
+	}
+
+	public RsaSecretEncryptor(PublicKey publicKey) {
+		this(DEFAULT_ENCODING, publicKey, null);
+	}
+
+	public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey) {
+		this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
+	}
+
+	public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm) {
+		this(encoding, publicKey, privateKey, algorithm, DEFAULT_SALT, false);
+	}
+
+	public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm,
+			String salt, boolean gcm) {
+		this.charset = Charset.forName(encoding);
+		this.publicKey = publicKey;
+		this.privateKey = privateKey;
+		this.defaultCharset = Charset.forName(DEFAULT_ENCODING);
+		this.algorithm = algorithm;
+		this.salt = isHex(salt) ? salt : new String(Hex.encode(salt.getBytes(this.defaultCharset)));
+		this.gcm = gcm;
+	}
+
+	@Override
+	public String getPublicKey() {
+		return RsaKeyHelper.encodePublicKey((RSAPublicKey) this.publicKey, "application");
+	}
+
+	@Override
+	public String encrypt(String text) {
+		return new String(Base64.getEncoder().encode(encrypt(text.getBytes(this.charset))), this.defaultCharset);
+	}
+
+	@Override
+	public String decrypt(String encryptedText) {
+		if (!canDecrypt()) {
+			throw new IllegalStateException("Encryptor is not configured for decryption");
+		}
+		return new String(decrypt(Base64.getDecoder().decode(encryptedText.getBytes(this.defaultCharset))),
+				this.charset);
+	}
+
+	@Override
+	public byte[] encrypt(byte[] byteArray) {
+		return encrypt(byteArray, this.publicKey, this.algorithm, this.salt, this.gcm);
+	}
+
+	@Override
+	public byte[] decrypt(byte[] encryptedByteArray) {
+		if (!canDecrypt()) {
+			throw new IllegalStateException("Encryptor is not configured for decryption");
+		}
+		return decrypt(encryptedByteArray, this.privateKey, this.algorithm, this.salt, this.gcm);
+	}
+
+	private static byte[] encrypt(byte[] text, PublicKey key, RsaAlgorithm alg, String salt, boolean gcm) {
+		byte[] random = KeyGenerators.secureRandom(16).generateKey();
+		BytesEncryptor aes = gcm ? Encryptors.stronger(new String(Hex.encode(random)), salt)
+				: Encryptors.standard(new String(Hex.encode(random)), salt);
+		try {
+			final Cipher cipher = Cipher.getInstance(alg.getJceName());
+			cipher.init(Cipher.ENCRYPT_MODE, key);
+			byte[] secret = cipher.doFinal(random);
+			ByteArrayOutputStream result = new ByteArrayOutputStream(text.length + 20);
+			writeInt(result, secret.length);
+			result.write(secret);
+			result.write(aes.encrypt(text));
+			return result.toByteArray();
+		}
+		catch (RuntimeException ex) {
+			throw ex;
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException("Cannot encrypt", ex);
+		}
+	}
+
+	private static void writeInt(ByteArrayOutputStream result, int length) throws IOException {
+		byte[] data = new byte[2];
+		data[0] = (byte) ((length >> 8) & 0xFF);
+		data[1] = (byte) (length & 0xFF);
+		result.write(data);
+	}
+
+	private static int readInt(ByteArrayInputStream result) throws IOException {
+		byte[] b = new byte[2];
+		result.read(b);
+		return ((b[0] & 0xFF) << 8) | (b[1] & 0xFF);
+	}
+
+	private static byte[] decrypt(byte[] text, PrivateKey key, RsaAlgorithm alg, String salt, boolean gcm) {
+		ByteArrayInputStream input = new ByteArrayInputStream(text);
+		ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
+		try {
+			int length = readInt(input);
+			byte[] random = new byte[length];
+			input.read(random);
+			final Cipher cipher = Cipher.getInstance(alg.getJceName());
+			cipher.init(Cipher.DECRYPT_MODE, key);
+			String secret = new String(Hex.encode(cipher.doFinal(random)));
+			byte[] buffer = new byte[text.length - random.length - 2];
+			input.read(buffer);
+			BytesEncryptor aes = gcm ? Encryptors.stronger(secret, salt) : Encryptors.standard(secret, salt);
+			output.write(aes.decrypt(buffer));
+			return output.toByteArray();
+		}
+		catch (RuntimeException ex) {
+			throw ex;
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException("Cannot decrypt", ex);
+		}
+	}
+
+	private static boolean isHex(String input) {
+		try {
+			Hex.decode(input);
+			return true;
+		}
+		catch (Exception ex) {
+			return false;
+		}
+	}
+
+	public boolean canDecrypt() {
+		return this.privateKey != null;
+	}
+
+}

+ 67 - 0
crypto/src/test/java/org/springframework/security/crypto/encrypt/KeyStoreKeyFactoryTests.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.io.ClassPathResource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class KeyStoreKeyFactoryTests {
+
+	@Test
+	public void initializeEncryptorFromKeyStore() {
+		char[] password = "foobar".toCharArray();
+		KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), password);
+		RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("test"));
+		assertThat(encryptor.canDecrypt()).as("Should be able to decrypt").isTrue();
+		assertThat(encryptor.decrypt(encryptor.encrypt("foo"))).isEqualTo("foo");
+	}
+
+	@Test
+	public void initializeEncryptorFromPkcs12KeyStore() {
+		char[] password = "letmein".toCharArray();
+		KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.pkcs12"), password);
+		RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("mytestkey"));
+		assertThat(encryptor.canDecrypt()).as("Should be able to decrypt").isTrue();
+		assertThat(encryptor.decrypt(encryptor.encrypt("foo"))).isEqualTo("foo");
+	}
+
+	@Test
+	public void initializeEncryptorFromTrustedCertificateInKeyStore() {
+		char[] password = "foobar".toCharArray();
+		KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), password);
+		RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("testcertificate"));
+		assertThat(encryptor.canDecrypt()).as("Should not be able to decrypt").isFalse();
+		assertThat(encryptor.encrypt("foo")).isNotEqualTo("foo");
+	}
+
+	@Test
+	public void initializeEncryptorFromTrustedCertificateInPkcs12KeyStore() {
+		char[] password = "letmein".toCharArray();
+		KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.pkcs12"), password);
+		RsaSecretEncryptor encryptor = new RsaSecretEncryptor(factory.getKeyPair("mytestcertificate"));
+		assertThat(encryptor.canDecrypt()).as("Should not be able to decrypt").isFalse();
+		assertThat(encryptor.encrypt("foo")).isNotEqualTo("foo");
+	}
+
+}

+ 64 - 0
crypto/src/test/java/org/springframework/security/crypto/encrypt/RsaKeyHelperTests.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.util.StreamUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RsaKeyHelperTests {
+
+	@Test
+	public void parsePrivateKey() throws Exception {
+		// ssh-keygen -m pem -b 1024 -f src/test/resources/fake.pem
+		String pem = StreamUtils.copyToString(new ClassPathResource("/fake.pem", getClass()).getInputStream(),
+				StandardCharsets.UTF_8);
+		KeyPair result = RsaKeyHelper.parseKeyPair(pem);
+		assertThat(result.getPrivate().getEncoded().length > 0).isTrue();
+		assertThat(result.getPrivate().getAlgorithm()).isEqualTo("RSA");
+	}
+
+	@Test
+	public void parseSpaceyKey() throws Exception {
+		String pem = StreamUtils.copyToString(new ClassPathResource("/spacey.pem", getClass()).getInputStream(),
+				StandardCharsets.UTF_8);
+		KeyPair result = RsaKeyHelper.parseKeyPair(pem);
+		assertThat(result.getPrivate().getEncoded().length > 0).isTrue();
+		assertThat(result.getPrivate().getAlgorithm()).isEqualTo("RSA");
+	}
+
+	@Test
+	public void parseBadKey() throws Exception {
+		// ssh-keygen -m pem -b 1024 -f src/test/resources/fake.pem
+		String pem = StreamUtils.copyToString(new ClassPathResource("/bad.pem", getClass()).getInputStream(),
+				StandardCharsets.UTF_8);
+		try {
+			RsaKeyHelper.parseKeyPair(pem);
+			throw new IllegalStateException("Expected IllegalArgumentException");
+		}
+		catch (IllegalArgumentException ex) {
+			assertThat(ex.getMessage().contains("PEM")).isTrue();
+		}
+	}
+
+}

+ 154 - 0
crypto/src/test/java/org/springframework/security/crypto/encrypt/RsaRawEncryptorTests.java

@@ -0,0 +1,154 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class RsaRawEncryptorTests {
+
+	private RsaRawEncryptor encryptor = new RsaRawEncryptor();
+
+	@BeforeEach
+	public void init() {
+		LONG_STRING = SHORT_STRING + SHORT_STRING + SHORT_STRING + SHORT_STRING;
+		for (int i = 0; i < 4; i++) {
+			LONG_STRING = LONG_STRING + LONG_STRING;
+		}
+	}
+
+	@Test
+	public void roundTrip() {
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripOeap() {
+		this.encryptor = new RsaRawEncryptor(RsaAlgorithm.OAEP);
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripLongString() {
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt(LONG_STRING))).isEqualTo(LONG_STRING);
+	}
+
+	@Test
+	public void roundTripLongStringOeap() {
+		this.encryptor = new RsaRawEncryptor(RsaAlgorithm.OAEP);
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt(LONG_STRING))).isEqualTo(LONG_STRING);
+	}
+
+	@Test
+	public void roundTrip2048Key() {
+		String pemData = "-----BEGIN RSA PRIVATE KEY-----"
+				+ "MIIEpQIBAAKCAQEA5KHEkCudAHCKIUHKyW6Z8dMyQsKrLbpDe0wDzx9MBARcOoS9"
+				+ "ZUjzXwK6p/0RM6aCp+b9kkr37QKQ9K/Am13sr0z8Mkn1Q2cvXiL5gbnY1nYGk8/m"
+				+ "CBX3QEhH2UII4yJsDVx1xmcSorZaWmeNKor7Zl3SZaQpWTvlkMgQKwY8DZL6PPxt"
+				+ "JRPeKmuUY6B59u5okh1G6Y9OnT2dVxAkqT8WgLHu6StxBmueJ272x2sUWUzoDhnP"
+				+ "7JRqa7h7t6fml3o3Op1iCywCOFzCIcK6G/oG/WZ7tbBYkwQdDjn/9VMdKkkPufwq"
+				+ "zt4S75NJygXDwDnNPiTVoaOwrRrL8ahgw6bFCQIDAQABAoIBAECIMHUI+l2fZj2Q"
+				+ "1m4Ym7cYB320eKCFjHqGsCSMDuarXGTgBp1KA/dzS8ASvAI6I3LEzhm2s1fge420"
+				+ "9cZksmOgdSa0nVeTDlmhwY8OJ9gQpDagXas2l/066Zy2+M8zbhAvYsbHXQk0MziF"
+				+ "NeEmLWNtY+9wcINRVrCQ549dSSIDK6UX21oU6d1mrlnF5/bbbdDIM3dKok355jwx"
+				+ "0HFY0tJIs1zArsBVoz3Ccu1MQEfnxEFM1LLPi5rE6cuHIOBinbD1OQ2R/HM2aukG"
+				+ "Rk2m6F3wAieJ7zpt5yaHuuIedn8p8m2NVulXAjgkY2oQl3GGiDH/H7eZlrvQRg6E"
+				+ "D8Bq+ykCgYEA+AfPXVeeVg3Qu0KsNrACek/o92BMY9g3GyPVGULGvq9seoNB86hj"
+				+ "nXasqngBfTlOfJFiahoEzRBB9hIyo1zMw4x99pR8nGxhR3aU+v8EGftMABGHWsB9"
+				+ "Jxj4YQH4fhi57iBa72QmNPbu/1o7y3SEe68E5PJ8KY3jc4xos8Vl658CgYEA6/pk"
+				+ "t6WZII+9lpxQfePQDIlBWAphiQceh995bGXfDmX3vOVmPozix9/fUtF1TeKS/ypw"
+				+ "u++Qmvj5oMsBVrjCyoOYfHKE2vGrLoEzkX/sPO65IsV00geZZoyCEKEE3USJfY46"
+				+ "u0hs61oP8HJyLhLiYiGcFTzZ4nEvvEbiM4E/DlcCgYEA6S0OecZhiK08SpAHrvIR"
+				+ "okN11PqnVkZyqAUr1a+9gI8TAKpdWmA4JlTnRuvDGqLBcsKLLwx+7voVyOyaxpH7"
+				+ "vutZkHNQIw6Q9co5jS4qAPMLJBVWlq7X+eWzvB9KKeG9Cm1IkD4q3Sg4z79Y75D+"
+				+ "6/hCNarxp29JIdwior81bikCgYEApp1P+b7pxGzZPvs1df2hCwjqY0BJJ5goPWVT"
+				+ "dW7kNGVYqz4JmAafpOJz6yTLP2fHxHRxzrBSmKlMj/RmCJZBqv2Jb+zn0zMpW5eM"
+				+ "EqKQ6WDgxSVH23fUHuz8dMNMDPL0ZPtEirGTfgVEFdCov9FDmGgErZYefVzPiI8/"
+				+ "7X/HRtcCgYEApQ2YS+0DLPqaM0cC6/6hDr/jmHLFhHaV6DZR7M9HHDnMN2uMlOEa"
+				+ "RYvXRMBjyQ7LQkwOj6K5k8MVrsDDM5dbekTBgcJMHfM9uViDkB0VPYULORmDJ20N"
+				+ "MLowIAiSon2B2/isatY80YtFq+bRyvPOzjGvinHN3MU1GH/gFuS0fiw=" + "-----END RSA PRIVATE KEY-----";
+		RsaRawEncryptor encryptor_2048 = new RsaRawEncryptor(pemData);
+		assertThat(encryptor_2048.decrypt(encryptor_2048.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTrip4096Key() {
+		String pemData = "-----BEGIN RSA PRIVATE KEY-----"
+				+ "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" + "-----END RSA PRIVATE KEY-----";
+		RsaRawEncryptor encryptor_4096 = new RsaRawEncryptor(pemData);
+		assertThat(encryptor_4096.decrypt(encryptor_4096.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	private static final String SHORT_STRING = "Bacon ipsum dolor sit amet tail pork loin pork chop filet mignon flank fatback tenderloin boudin shankle corned beef t-bone short ribs. Meatball capicola ball tip short loin beef ribs shoulder, kielbasa pork chop meatloaf biltong porchetta bresaola t-bone spare ribs. Andouille t-bone sausage ground round frankfurter venison. Ground round meatball chicken ribeye doner tongue porchetta.";
+
+	private static String LONG_STRING;
+
+}

+ 121 - 0
crypto/src/test/java/org/springframework/security/crypto/encrypt/RsaSecretEncryptorTests.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright 2013-2024 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.crypto.encrypt;
+
+import java.security.PublicKey;
+import java.security.interfaces.RSAPublicKey;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class RsaSecretEncryptorTests {
+
+	private RsaSecretEncryptor encryptor = new RsaSecretEncryptor();
+
+	@BeforeEach
+	public void init() {
+		LONG_STRING = SHORT_STRING + SHORT_STRING + SHORT_STRING + SHORT_STRING;
+		for (int i = 0; i < 4; i++) {
+			LONG_STRING = LONG_STRING + LONG_STRING;
+		}
+	}
+
+	@Test
+	public void roundTripKey() {
+		PublicKey key = RsaKeyHelper.generateKeyPair().getPublic();
+		String encoded = RsaKeyHelper.encodePublicKey((RSAPublicKey) key, "application");
+		assertThat(RsaKeyHelper.parsePublicKey(encoded)).isEqualTo(key);
+	}
+
+	@Test
+	public void roundTrip() {
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripWithSalt() {
+		this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, "somesalt");
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripWithHexSalt() {
+		this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, "beefea");
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripWithLongSalt() {
+		this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, "somesaltsomesaltsomesaltsomesaltsomesalt");
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripOaep() {
+		this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripOaepGcm() {
+		this.encryptor = new RsaSecretEncryptor(RsaAlgorithm.OAEP, true);
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void roundTripWithMixedAlgorithm() {
+		RsaSecretEncryptor oaep = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
+		assertThatIllegalStateException().isThrownBy(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")));
+	}
+
+	@Test
+	public void roundTripWithMixedSalt() {
+		RsaSecretEncryptor other = new RsaSecretEncryptor(this.encryptor.getPublicKey(), RsaAlgorithm.DEFAULT, "salt");
+		assertThatIllegalStateException().isThrownBy(() -> this.encryptor.decrypt(other.encrypt("encryptor")));
+	}
+
+	@Test
+	public void roundTripWithPublicKeyEncryption() {
+		RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
+		RsaSecretEncryptor decryptor = this.encryptor;
+		assertThat(decryptor.decrypt(encryptor.encrypt("encryptor"))).isEqualTo("encryptor");
+	}
+
+	@Test
+	public void publicKeyCannotDecrypt() {
+		RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
+		assertThat(encryptor.canDecrypt()).as("Encryptor schould not be able to decrypt").isFalse();
+		assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encryptor.encrypt("encryptor")));
+	}
+
+	@Test
+	public void roundTripLongString() {
+		assertThat(this.encryptor.decrypt(this.encryptor.encrypt(LONG_STRING))).isEqualTo(LONG_STRING);
+	}
+
+	private static final String SHORT_STRING = "Bacon ipsum dolor sit amet tail pork loin pork chop filet mignon flank fatback tenderloin boudin shankle corned beef t-bone short ribs. Meatball capicola ball tip short loin beef ribs shoulder, kielbasa pork chop meatloaf biltong porchetta bresaola t-bone spare ribs. Andouille t-bone sausage ground round frankfurter venison. Ground round meatball chicken ribeye doner tongue porchetta.";
+
+	private static String LONG_STRING;
+
+}

+ 2 - 0
crypto/src/test/resources/bad.pem

@@ -0,0 +1,2 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAwClFgrRa/PUHPIJr9gvIPL6g6Rjp/TVZmVNOf2fL96DYbkj5

+ 15 - 0
crypto/src/test/resources/fake.pem

@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICWwIBAAKBgQDMWnfaQ0yLFXelprq2S8UurnaGvxFNUdbmTyJeycem5vGLycEY
+T4KcdVCTU5491cjbk5GcHjoj2efRSO0y0aXIlUJpLofDdML/SuGLZWp/GbEv978M
+pZIztK8iaIm7D/D7by8aws1RJyD9T+lZDAGY7eFfMp0EQyHOcEL0NGFLuwIDAQAB
+AoGAWwC6uO8ZaiKwOouqQD4z3FsDG3SA/v7ABaYd9zpCd9gGnyrEm8/kqUoxDLrD
+EGRg4y+vO2fWmlqSuoeQYf4spf+vi2di+mGIb6nGe7TpMLPa7lFLOSQHZRx5M5H6
+JDhfhAHlKmF9gLGvDHbpyErzn5YXjcu0PoFiNC1y445D8iECQQDvJzkGbJ9l9vb0
+oRyGXRDpddUcVMECLLB9NKmTl/zKy/qVPD+zYNoi87ePBJFbgmAXRjhhTk2uSBRP
+NtVaMoXLAkEA2r+ugzjsLZQIYz/9gxdzdbKWDgpSPbhKCR4bOmrDgJMcOVjtwW+n
++liaX6zwI0QEgCAWLzCbbYDmj3kJrRwT0QJAaowg/dm7EmR7FfYJjVs9Q6X5skuY
+Se27G60wt88JExjZpU9YWgSWaugGKbOxRwHI6dWhHMkUFseKNNiLKUpFDQJALIGP
+ahdsxiE2S6s7Uy60SSAas6SZ8wDJ320GsS4DtOc5eNmFFjQ3gxH/5rNy8FnoaIEe
+wl8rYG43er1voI7z4QJAB4qaqBo7eeiRgnUVIccaSZkNIMSrZ9QUjVFRgfLwAXDO
+Ae+t6V+eB0oaIXczA+BLj3Oe6D3iHRGHrxGlcvDdHw==
+-----END RSA PRIVATE KEY-----

BIN
crypto/src/test/resources/keystore.jks


BIN
crypto/src/test/resources/keystore.pkcs12


+ 25 - 0
crypto/src/test/resources/spacey.pem

@@ -0,0 +1,25 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAwClFgrRa/PUHPIJr9gvIPL6g6Rjp/TVZmVNOf2fL96DYbkj5
+4YbrwfKwjoTjk1M6gLQpOA4Blocx6zN5OnICnVGlVM9xymWxTxxCfc2tE2Fai9I1wchULCChhwm/UU5ZNi3KpXinlyamSYw+lMQkZ8gTXCgOEvs2j9E1quF4pvy1BZKvbD8tUnUQlyiKRnI6gOxQL8B6OAYPRdaa9FVNmrs1B4eDPG918L2f1pT090P1n+tw
+iejNgQvtSD78/A88qt89OhzscsufALTrBjycn89kkfBd0zbVLF0W6+ZVLZrf97/y
+LCoGSCcZL9LFPNvNqxOnleviDco7aOs4stQ9jQIDAQABAoIBAQC1TbthyN0YUe+T
+        7dIDAbbZaVrU00biOtXgzjMADmTprP7Hf18UpIIIKfzfWw6FUD+gc1t4oe5pogE9
+UwGMXUmOORxu2pMYTb5vT9CEdexYnsAZsCo8PdD9GYSNrmquQef2MFpEqYQmHrdC
+                KWpaXn2i1ak+iCRPUGp4YwHpynZVxfE8z/AIsPn6NPDh6SnCXb1rTgQe2UCfXm93
+UJe5F/OR2kQi5KFO+dxLmCOBCwr6SGCLH+VotGpuxCVRUd9sJ/d4QpDZEgjuf7Ug
+eQHfgMDS/tc09B9rl0dwKnEa31kcQ9X9KLkKP+w0Pqhh0Emny20eg9jS6XNayg61
+p/LQtW9BAoGBAO5veKMIcXfZmuh11WIIhdhLKkNtYyt5NDmrV8/IVScLFvjB0ftt
+8PAtXo/ekOHkyITyIumQ9l4VCvacNw7DyV9FYk4WvrvVYOCL8aZi+O5+12NT67eO
+Rr/voGlRoV05X7+inc90qbbYJ8lRmLSqvzmsm98mkuhw/FKGRhVZIfAJAoGBAM5R
+            I5vK6cJxOwXQOEGOd5/8B9JMFXyuendXo/N2/NxSQsbx4pc3v2rv/eGJYaY7Nx/y
+2M/vdWYkpG59PAS3k2TrCA/0SGmyVqY+c8BomKisU5VaBlIPfGuec9tDPgWCp8Ur
+3Jjt/2sVoa0vMkqymUqMb9HyH9tdI9oyh7EOOrplAoGAR6DlNNUMgVy11K/Rcqns
+y5WJFMh/ykeXENwQfTNJoXkLZZ+UXVwhzYVTqxTJoZMBSi8TnecWnBzmNj+nqp/W
+    lvBZH+xlUDhB6jMgXUPOVJd2TTigz3vGdVKfdgQ33bGmugM4NWJuuacmDKyem2fQ
+    GptoGBmWeI24v3HnC/LC50ECgYAz0iN8hRnz0db+Xc9TgAJB997LDnszJuvxv9yZ
+    UWCvwiWtrKG6U7FLnd4J4STayPLOnoOgrsexETEP43rIwIdQCMysnTH3AmlLNlKC
+    mIMHksknsUX3JJaevVziTOBuJ+QV3S96ZgUKk5NZWYprQrLIC8AmXodr5NgVfS2h
+    5i4QFQKBgFfbYHiMw5AAUQrBNkrAjLd1wIaO/6qS3w4OsCWKowhfaJLEXAbIRV7s
+vAtgtlCovdasVj4RRLXFf+73naVTQjBZI+3jWHHyFk3+Zy86mQCSGv9WuDVV1IhS
+h8InTVvK8wgdgX7qiw3pvU0roqNW4/j4j8OqJO3Zt4KO2iX8htsO
+-----END RSA PRIVATE KEY-----

+ 4 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -11,3 +11,7 @@ Below are the highlights of the release.
 == CAS
 
 - https://github.com/spring-projects/spring-security/pull/14193[gh-14193] - Added support for CAS Gateway Authentication
+
+== Crypto
+
+- https://github.com/spring-projects/spring-security/issues/14202[gh-14202] - Migrated spring-security-rsa into spring-security-crypto