Browse Source

Add Nullability to spring-security-crypto

Closes gh-17533
Rob Winch 1 month ago
parent
commit
9db1ffbd79
22 changed files with 237 additions and 37 deletions
  1. 4 0
      crypto/spring-security-crypto.gradle
  2. 20 0
      crypto/src/main/java/org/springframework/security/crypto/argon2/package-info.java
  3. 2 2
      crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java
  4. 5 4
      crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java
  5. 20 0
      crypto/src/main/java/org/springframework/security/crypto/bcrypt/package-info.java
  6. 3 1
      crypto/src/main/java/org/springframework/security/crypto/codec/Utf8.java
  7. 3 0
      crypto/src/main/java/org/springframework/security/crypto/codec/package-info.java
  8. 6 3
      crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java
  9. 4 1
      crypto/src/main/java/org/springframework/security/crypto/encrypt/CipherUtils.java
  10. 3 1
      crypto/src/main/java/org/springframework/security/crypto/encrypt/KeyStoreKeyFactory.java
  11. 2 1
      crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaKeyHelper.java
  12. 11 5
      crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaRawEncryptor.java
  13. 9 6
      crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaSecretEncryptor.java
  14. 20 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/package-info.java
  15. 20 0
      crypto/src/main/java/org/springframework/security/crypto/factory/package-info.java
  16. 20 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/package-info.java
  17. 12 4
      crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java
  18. 9 7
      crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java
  19. 4 2
      crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java
  20. 20 0
      crypto/src/main/java/org/springframework/security/crypto/password/package-info.java
  21. 20 0
      crypto/src/main/java/org/springframework/security/crypto/scrypt/package-info.java
  22. 20 0
      crypto/src/main/java/org/springframework/security/crypto/util/package-info.java

+ 4 - 0
crypto/spring-security-crypto.gradle

@@ -1,3 +1,7 @@
+plugins {
+	id 'security-nullability'
+}
+
 apply plugin: 'io.spring.convention.spring-module'
 apply plugin: 'io.spring.convention.spring-module'
 
 
 dependencies {
 dependencies {

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/argon2/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.argon2;
+
+import org.jspecify.annotations.NullMarked;

+ 2 - 2
crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java

@@ -210,9 +210,9 @@ public class BCrypt {
 	static final int MAX_LOG_ROUNDS = 31;
 	static final int MAX_LOG_ROUNDS = 31;
 
 
 	// Expanded Blowfish key
 	// Expanded Blowfish key
-	private int P[];
+	private int P[] = new int[0];
 
 
-	private int S[];
+	private int S[] = new int[0];
 
 
 	/**
 	/**
 	 * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note
 	 * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note

+ 5 - 4
crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java

@@ -22,6 +22,7 @@ import java.util.regex.Pattern;
 
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.commons.logging.LogFactory;
+import org.jspecify.annotations.Nullable;
 
 
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 
 
@@ -43,7 +44,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
 
 
 	private final BCryptVersion version;
 	private final BCryptVersion version;
 
 
-	private final SecureRandom random;
+	private final @Nullable SecureRandom random;
 
 
 	public BCryptPasswordEncoder() {
 	public BCryptPasswordEncoder() {
 		this(-1);
 		this(-1);
@@ -67,7 +68,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
 	 * @param version the version of bcrypt, can be 2a,2b,2y
 	 * @param version the version of bcrypt, can be 2a,2b,2y
 	 * @param random the secure random instance to use
 	 * @param random the secure random instance to use
 	 */
 	 */
-	public BCryptPasswordEncoder(BCryptVersion version, SecureRandom random) {
+	public BCryptPasswordEncoder(BCryptVersion version, @Nullable SecureRandom random) {
 		this(version, -1, random);
 		this(version, -1, random);
 	}
 	}
 
 
@@ -75,7 +76,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
 	 * @param strength the log rounds to use, between 4 and 31
 	 * @param strength the log rounds to use, between 4 and 31
 	 * @param random the secure random instance to use
 	 * @param random the secure random instance to use
 	 */
 	 */
-	public BCryptPasswordEncoder(int strength, SecureRandom random) {
+	public BCryptPasswordEncoder(int strength, @Nullable SecureRandom random) {
 		this(BCryptVersion.$2A, strength, random);
 		this(BCryptVersion.$2A, strength, random);
 	}
 	}
 
 
@@ -92,7 +93,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
 	 * @param strength the log rounds to use, between 4 and 31
 	 * @param strength the log rounds to use, between 4 and 31
 	 * @param random the secure random instance to use
 	 * @param random the secure random instance to use
 	 */
 	 */
-	public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
+	public BCryptPasswordEncoder(BCryptVersion version, int strength, @Nullable SecureRandom random) {
 		if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
 		if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
 			throw new IllegalArgumentException("Bad strength");
 			throw new IllegalArgumentException("Bad strength");
 		}
 		}

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/bcrypt/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.bcrypt;
+
+import org.jspecify.annotations.NullMarked;

+ 3 - 1
crypto/src/main/java/org/springframework/security/crypto/codec/Utf8.java

@@ -22,6 +22,8 @@ import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.charset.StandardCharsets;
 
 
+import org.jspecify.annotations.Nullable;
+
 /**
 /**
  * UTF-8 Charset encoder/decoder.
  * UTF-8 Charset encoder/decoder.
  * <p>
  * <p>
@@ -39,7 +41,7 @@ public final class Utf8 {
 	/**
 	/**
 	 * Get the bytes of the String in UTF-8 encoded form.
 	 * Get the bytes of the String in UTF-8 encoded form.
 	 */
 	 */
-	public static byte[] encode(CharSequence string) {
+	public static byte[] encode(@Nullable CharSequence string) {
 		try {
 		try {
 			ByteBuffer bytes = CHARSET.newEncoder().encode(CharBuffer.wrap(string));
 			ByteBuffer bytes = CHARSET.newEncoder().encode(CharBuffer.wrap(string));
 			byte[] bytesCopy = new byte[bytes.limit()];
 			byte[] bytesCopy = new byte[bytes.limit()];

+ 3 - 0
crypto/src/main/java/org/springframework/security/crypto/codec/package-info.java

@@ -17,4 +17,7 @@
 /**
 /**
  * Internal codec classes. Only intended for use within the framework.
  * Internal codec classes. Only intended for use within the framework.
  */
  */
+@NullMarked
 package org.springframework.security.crypto.codec;
 package org.springframework.security.crypto.codec;
+
+import org.jspecify.annotations.NullMarked;

+ 6 - 3
crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java

@@ -25,6 +25,8 @@ import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.PBEKeySpec;
 import javax.crypto.spec.PBEKeySpec;
 import javax.crypto.spec.SecretKeySpec;
 import javax.crypto.spec.SecretKeySpec;
 
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.crypto.codec.Hex;
 import org.springframework.security.crypto.codec.Hex;
 import org.springframework.security.crypto.keygen.BytesKeyGenerator;
 import org.springframework.security.crypto.keygen.BytesKeyGenerator;
 import org.springframework.security.crypto.keygen.KeyGenerators;
 import org.springframework.security.crypto.keygen.KeyGenerators;
@@ -81,7 +83,7 @@ public final class AesBytesEncryptor implements BytesEncryptor {
 	 * @param salt the hex-encoded salt value
 	 * @param salt the hex-encoded salt value
 	 * @param ivGenerator the generator used to generate the initialization vector
 	 * @param ivGenerator the generator used to generate the initialization vector
 	 */
 	 */
-	public AesBytesEncryptor(String password, CharSequence salt, BytesKeyGenerator ivGenerator) {
+	public AesBytesEncryptor(String password, CharSequence salt, @Nullable BytesKeyGenerator ivGenerator) {
 		this(password, salt, ivGenerator, CipherAlgorithm.CBC);
 		this(password, salt, ivGenerator, CipherAlgorithm.CBC);
 	}
 	}
 
 
@@ -95,7 +97,8 @@ public final class AesBytesEncryptor implements BytesEncryptor {
 	 * @param ivGenerator the generator used to generate the initialization vector
 	 * @param ivGenerator the generator used to generate the initialization vector
 	 * @param alg the {@link CipherAlgorithm} to be used
 	 * @param alg the {@link CipherAlgorithm} to be used
 	 */
 	 */
-	public AesBytesEncryptor(String password, CharSequence salt, BytesKeyGenerator ivGenerator, CipherAlgorithm alg) {
+	public AesBytesEncryptor(String password, CharSequence salt, @Nullable BytesKeyGenerator ivGenerator,
+			CipherAlgorithm alg) {
 		this(CipherUtils.newSecretKey("PBKDF2WithHmacSHA1",
 		this(CipherUtils.newSecretKey("PBKDF2WithHmacSHA1",
 				new PBEKeySpec(password.toCharArray(), Hex.decode(salt), 1024, 256)), ivGenerator, alg);
 				new PBEKeySpec(password.toCharArray(), Hex.decode(salt), 1024, 256)), ivGenerator, alg);
 	}
 	}
@@ -108,7 +111,7 @@ public final class AesBytesEncryptor implements BytesEncryptor {
 	 * {@link CipherAlgorithm}
 	 * {@link CipherAlgorithm}
 	 * @param alg the {@link CipherAlgorithm} to be used
 	 * @param alg the {@link CipherAlgorithm} to be used
 	 */
 	 */
-	public AesBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator, CipherAlgorithm alg) {
+	public AesBytesEncryptor(SecretKey secretKey, @Nullable BytesKeyGenerator ivGenerator, CipherAlgorithm alg) {
 		this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
 		this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
 		this.alg = alg;
 		this.alg = alg;
 		this.encryptor = alg.createCipher();
 		this.encryptor = alg.createCipher();

+ 4 - 1
crypto/src/main/java/org/springframework/security/crypto/encrypt/CipherUtils.java

@@ -32,6 +32,8 @@ import javax.crypto.SecretKeyFactory;
 import javax.crypto.spec.PBEKeySpec;
 import javax.crypto.spec.PBEKeySpec;
 import javax.crypto.spec.PBEParameterSpec;
 import javax.crypto.spec.PBEParameterSpec;
 
 
+import org.jspecify.annotations.Nullable;
+
 /**
 /**
  * Static helper for working with the Cipher API.
  * Static helper for working with the Cipher API.
  *
  *
@@ -109,7 +111,8 @@ final class CipherUtils {
 	/**
 	/**
 	 * Initializes the Cipher for use.
 	 * Initializes the Cipher for use.
 	 */
 	 */
-	static void initCipher(Cipher cipher, int mode, SecretKey secretKey, AlgorithmParameterSpec parameterSpec) {
+	static void initCipher(Cipher cipher, int mode, SecretKey secretKey,
+			@Nullable AlgorithmParameterSpec parameterSpec) {
 		try {
 		try {
 			if (parameterSpec != null) {
 			if (parameterSpec != null) {
 				cipher.init(mode, secretKey, parameterSpec);
 				cipher.init(mode, secretKey, parameterSpec);

+ 3 - 1
crypto/src/main/java/org/springframework/security/crypto/encrypt/KeyStoreKeyFactory.java

@@ -25,6 +25,8 @@ import java.security.cert.Certificate;
 import java.security.interfaces.RSAPrivateCrtKey;
 import java.security.interfaces.RSAPrivateCrtKey;
 import java.security.spec.RSAPublicKeySpec;
 import java.security.spec.RSAPublicKeySpec;
 
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.Resource;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 
 
@@ -39,7 +41,7 @@ public class KeyStoreKeyFactory {
 
 
 	private final char[] password;
 	private final char[] password;
 
 
-	private KeyStore store;
+	private @Nullable KeyStore store;
 
 
 	private final Object lock = new Object();
 	private final Object lock = new Object();
 
 

+ 2 - 1
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaKeyHelper.java

@@ -44,6 +44,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
 import org.bouncycastle.asn1.ASN1Sequence;
 import org.bouncycastle.asn1.ASN1Sequence;
+import org.jspecify.annotations.Nullable;
 
 
 /**
 /**
  * Reads RSA key pairs using BC provider classes but without the need to specify a crypto
  * Reads RSA key pairs using BC provider classes but without the need to specify a crypto
@@ -164,7 +165,7 @@ final class RsaKeyHelper {
 
 
 	private static final Pattern SSH_PUB_KEY = Pattern.compile("ssh-(rsa|dsa) ([A-Za-z0-9/+]+=*) (.*)");
 	private static final Pattern SSH_PUB_KEY = Pattern.compile("ssh-(rsa|dsa) ([A-Za-z0-9/+]+=*) (.*)");
 
 
-	private static RSAPublicKey extractPublicKey(String key) {
+	private static @Nullable RSAPublicKey extractPublicKey(String key) {
 
 
 		Matcher m = SSH_PUB_KEY.matcher(key);
 		Matcher m = SSH_PUB_KEY.matcher(key);
 
 

+ 11 - 5
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaRawEncryptor.java

@@ -28,6 +28,8 @@ import java.util.Base64;
 
 
 import javax.crypto.Cipher;
 import javax.crypto.Cipher;
 
 
+import org.jspecify.annotations.Nullable;
+
 /**
 /**
  * @author Dave Syer
  * @author Dave Syer
  * @since 6.3
  * @since 6.3
@@ -42,7 +44,7 @@ public class RsaRawEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHol
 
 
 	private RSAPublicKey publicKey;
 	private RSAPublicKey publicKey;
 
 
-	private RSAPrivateKey privateKey;
+	private @Nullable RSAPrivateKey privateKey;
 
 
 	private Charset defaultCharset;
 	private Charset defaultCharset;
 
 
@@ -70,11 +72,12 @@ public class RsaRawEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHol
 		this(DEFAULT_ENCODING, publicKey, null);
 		this(DEFAULT_ENCODING, publicKey, null);
 	}
 	}
 
 
-	public RsaRawEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey) {
+	public RsaRawEncryptor(String encoding, PublicKey publicKey, @Nullable PrivateKey privateKey) {
 		this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
 		this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
 	}
 	}
 
 
-	public RsaRawEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm) {
+	public RsaRawEncryptor(String encoding, PublicKey publicKey, @Nullable PrivateKey privateKey,
+			RsaAlgorithm algorithm) {
 		this.charset = Charset.forName(encoding);
 		this.charset = Charset.forName(encoding);
 		this.publicKey = (RSAPublicKey) publicKey;
 		this.publicKey = (RSAPublicKey) publicKey;
 		this.privateKey = (RSAPrivateKey) privateKey;
 		this.privateKey = (RSAPrivateKey) privateKey;
@@ -135,7 +138,7 @@ public class RsaRawEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHol
 		}
 		}
 	}
 	}
 
 
-	private static byte[] decrypt(byte[] text, RSAPrivateKey key, RsaAlgorithm alg) {
+	private static byte[] decrypt(byte[] text, @Nullable RSAPrivateKey key, RsaAlgorithm alg) {
 		ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
 		ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
 		try {
 		try {
 			final Cipher cipher = Cipher.getInstance(alg.getJceName());
 			final Cipher cipher = Cipher.getInstance(alg.getJceName());
@@ -160,7 +163,10 @@ public class RsaRawEncryptor implements BytesEncryptor, TextEncryptor, RsaKeyHol
 	}
 	}
 
 
 	// copied from sun.security.rsa.RSACore.getByteLength(java.math.BigInteger)
 	// copied from sun.security.rsa.RSACore.getByteLength(java.math.BigInteger)
-	public static int getByteLength(RSAKey key) {
+	public static int getByteLength(@Nullable RSAKey key) {
+		if (key == null) {
+			throw new IllegalArgumentException("key cannot be null");
+		}
 		int n = key.getModulus().bitLength();
 		int n = key.getModulus().bitLength();
 		return (n + 7) >> 3;
 		return (n + 7) >> 3;
 	}
 	}

+ 9 - 6
crypto/src/main/java/org/springframework/security/crypto/encrypt/RsaSecretEncryptor.java

@@ -28,6 +28,8 @@ import java.util.Base64;
 
 
 import javax.crypto.Cipher;
 import javax.crypto.Cipher;
 
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.crypto.codec.Hex;
 import org.springframework.security.crypto.codec.Hex;
 import org.springframework.security.crypto.keygen.KeyGenerators;
 import org.springframework.security.crypto.keygen.KeyGenerators;
 
 
@@ -50,7 +52,7 @@ public class RsaSecretEncryptor implements BytesEncryptor, TextEncryptor, RsaKey
 
 
 	private final PublicKey publicKey;
 	private final PublicKey publicKey;
 
 
-	private final PrivateKey privateKey;
+	private final @Nullable PrivateKey privateKey;
 
 
 	private final Charset defaultCharset;
 	private final Charset defaultCharset;
 
 
@@ -120,16 +122,17 @@ public class RsaSecretEncryptor implements BytesEncryptor, TextEncryptor, RsaKey
 		this(DEFAULT_ENCODING, publicKey, null);
 		this(DEFAULT_ENCODING, publicKey, null);
 	}
 	}
 
 
-	public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey) {
+	public RsaSecretEncryptor(String encoding, PublicKey publicKey, @Nullable PrivateKey privateKey) {
 		this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
 		this(encoding, publicKey, privateKey, RsaAlgorithm.DEFAULT);
 	}
 	}
 
 
-	public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm) {
+	public RsaSecretEncryptor(String encoding, PublicKey publicKey, @Nullable PrivateKey privateKey,
+			RsaAlgorithm algorithm) {
 		this(encoding, publicKey, privateKey, algorithm, DEFAULT_SALT, false);
 		this(encoding, publicKey, privateKey, algorithm, DEFAULT_SALT, false);
 	}
 	}
 
 
-	public RsaSecretEncryptor(String encoding, PublicKey publicKey, PrivateKey privateKey, RsaAlgorithm algorithm,
-			String salt, boolean gcm) {
+	public RsaSecretEncryptor(String encoding, PublicKey publicKey, @Nullable PrivateKey privateKey,
+			RsaAlgorithm algorithm, String salt, boolean gcm) {
 		this.charset = Charset.forName(encoding);
 		this.charset = Charset.forName(encoding);
 		this.publicKey = publicKey;
 		this.publicKey = publicKey;
 		this.privateKey = privateKey;
 		this.privateKey = privateKey;
@@ -206,7 +209,7 @@ public class RsaSecretEncryptor implements BytesEncryptor, TextEncryptor, RsaKey
 		return ((b[0] & 0xFF) << 8) | (b[1] & 0xFF);
 		return ((b[0] & 0xFF) << 8) | (b[1] & 0xFF);
 	}
 	}
 
 
-	private static byte[] decrypt(byte[] text, PrivateKey key, RsaAlgorithm alg, String salt, boolean gcm) {
+	private static byte[] decrypt(byte[] text, @Nullable PrivateKey key, RsaAlgorithm alg, String salt, boolean gcm) {
 		ByteArrayInputStream input = new ByteArrayInputStream(text);
 		ByteArrayInputStream input = new ByteArrayInputStream(text);
 		ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
 		ByteArrayOutputStream output = new ByteArrayOutputStream(text.length);
 		try {
 		try {

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.encrypt;
+
+import org.jspecify.annotations.NullMarked;

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/factory/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.factory;
+
+import org.jspecify.annotations.NullMarked;

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.keygen;
+
+import org.jspecify.annotations.NullMarked;

+ 12 - 4
crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java

@@ -19,6 +19,8 @@ package org.springframework.security.crypto.password;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
+import org.jspecify.annotations.Nullable;
+
 /**
 /**
  * A password encoder that delegates to another PasswordEncoder based upon a prefixed
  * A password encoder that delegates to another PasswordEncoder based upon a prefixed
  * identifier.
  * identifier.
@@ -146,7 +148,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 
 
 	private final PasswordEncoder passwordEncoderForEncode;
 	private final PasswordEncoder passwordEncoderForEncode;
 
 
-	private final Map<String, PasswordEncoder> idToPasswordEncoder;
+	private final Map<@Nullable String, PasswordEncoder> idToPasswordEncoder;
 
 
 	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
 	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
 
 
@@ -232,6 +234,9 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 
 
 	@Override
 	@Override
 	public String encode(CharSequence rawPassword) {
 	public String encode(CharSequence rawPassword) {
+		if (rawPassword == null) {
+			throw new IllegalArgumentException("rawPassword cannot be null");
+		}
 		return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
 		return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
 	}
 	}
 
 
@@ -249,7 +254,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 		return delegate.matches(rawPassword, encodedPassword);
 		return delegate.matches(rawPassword, encodedPassword);
 	}
 	}
 
 
-	private String extractId(String prefixEncodedPassword) {
+	private @Nullable String extractId(@Nullable String prefixEncodedPassword) {
 		if (prefixEncodedPassword == null) {
 		if (prefixEncodedPassword == null) {
 			return null;
 			return null;
 		}
 		}
@@ -265,14 +270,17 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
 	}
 	}
 
 
 	@Override
 	@Override
-	public boolean upgradeEncoding(String prefixEncodedPassword) {
+	public boolean upgradeEncoding(@Nullable String prefixEncodedPassword) {
+		if (prefixEncodedPassword == null) {
+			return false;
+		}
 		String id = extractId(prefixEncodedPassword);
 		String id = extractId(prefixEncodedPassword);
 		if (!this.idForEncode.equalsIgnoreCase(id)) {
 		if (!this.idForEncode.equalsIgnoreCase(id)) {
 			return true;
 			return true;
 		}
 		}
 		else {
 		else {
 			String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
 			String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
-			return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
+			return this.passwordEncoderForEncode.upgradeEncoding(encodedPassword);
 		}
 		}
 	}
 	}
 
 

+ 9 - 7
crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java

@@ -20,6 +20,8 @@ import java.security.MessageDigest;
 import java.util.Base64;
 import java.util.Base64;
 import java.util.Locale;
 import java.util.Locale;
 
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.crypto.codec.Utf8;
 import org.springframework.security.crypto.codec.Utf8;
 import org.springframework.security.crypto.keygen.BytesKeyGenerator;
 import org.springframework.security.crypto.keygen.BytesKeyGenerator;
 import org.springframework.security.crypto.keygen.KeyGenerators;
 import org.springframework.security.crypto.keygen.KeyGenerators;
@@ -72,7 +74,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
 		this.saltGenerator = saltGenerator;
 		this.saltGenerator = saltGenerator;
 	}
 	}
 
 
-	private byte[] combineHashAndSalt(byte[] hash, byte[] salt) {
+	private byte[] combineHashAndSalt(byte[] hash, byte @Nullable [] salt) {
 		if (salt == null) {
 		if (salt == null) {
 			return hash;
 			return hash;
 		}
 		}
@@ -96,7 +98,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
 		return encode(rawPass, salt);
 		return encode(rawPass, salt);
 	}
 	}
 
 
-	private String encode(CharSequence rawPassword, byte[] salt) {
+	private String encode(@Nullable CharSequence rawPassword, byte @Nullable [] salt) {
 		MessageDigest sha = getSha(rawPassword);
 		MessageDigest sha = getSha(rawPassword);
 		if (salt != null) {
 		if (salt != null) {
 			sha.update(salt);
 			sha.update(salt);
@@ -106,7 +108,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
 		return prefix + Utf8.decode(Base64.getEncoder().encode(hash));
 		return prefix + Utf8.decode(Base64.getEncoder().encode(hash));
 	}
 	}
 
 
-	private MessageDigest getSha(CharSequence rawPassword) {
+	private MessageDigest getSha(@Nullable CharSequence rawPassword) {
 		try {
 		try {
 			MessageDigest sha = MessageDigest.getInstance("SHA");
 			MessageDigest sha = MessageDigest.getInstance("SHA");
 			sha.update(Utf8.encode(rawPassword));
 			sha.update(Utf8.encode(rawPassword));
@@ -117,7 +119,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
 		}
 		}
 	}
 	}
 
 
-	private String getPrefix(byte[] salt) {
+	private String getPrefix(byte @Nullable [] salt) {
 		if (salt == null || salt.length == 0) {
 		if (salt == null || salt.length == 0) {
 			return this.forceLowerCasePrefix ? SHA_PREFIX_LC : SHA_PREFIX;
 			return this.forceLowerCasePrefix ? SHA_PREFIX_LC : SHA_PREFIX;
 		}
 		}
@@ -145,7 +147,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
 		return matches((rawPassword != null) ? rawPassword.toString() : null, encodedPassword);
 		return matches((rawPassword != null) ? rawPassword.toString() : null, encodedPassword);
 	}
 	}
 
 
-	private boolean matches(String rawPassword, String encodedPassword) {
+	private boolean matches(@Nullable String rawPassword, String encodedPassword) {
 		String prefix = extractPrefix(encodedPassword);
 		String prefix = extractPrefix(encodedPassword);
 		if (prefix == null) {
 		if (prefix == null) {
 			return PasswordEncoderUtils.equals(encodedPassword, rawPassword);
 			return PasswordEncoderUtils.equals(encodedPassword, rawPassword);
@@ -156,7 +158,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
 		return PasswordEncoderUtils.equals(encodedRawPass, encodedPassword.substring(startOfHash));
 		return PasswordEncoderUtils.equals(encodedRawPass, encodedPassword.substring(startOfHash));
 	}
 	}
 
 
-	private byte[] getSalt(String encodedPassword, String prefix) {
+	private byte @Nullable [] getSalt(String encodedPassword, String prefix) {
 		if (prefix.equals(SSHA_PREFIX) || prefix.equals(SSHA_PREFIX_LC)) {
 		if (prefix.equals(SSHA_PREFIX) || prefix.equals(SSHA_PREFIX_LC)) {
 			return extractSalt(encodedPassword);
 			return extractSalt(encodedPassword);
 		}
 		}
@@ -170,7 +172,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
 	/**
 	/**
 	 * Returns the hash prefix or null if there isn't one.
 	 * Returns the hash prefix or null if there isn't one.
 	 */
 	 */
-	private String extractPrefix(String encPass) {
+	private @Nullable String extractPrefix(String encPass) {
 		if (!encPass.startsWith("{")) {
 		if (!encPass.startsWith("{")) {
 			return null;
 			return null;
 		}
 		}

+ 4 - 2
crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java

@@ -18,6 +18,8 @@ package org.springframework.security.crypto.password;
 
 
 import java.security.MessageDigest;
 import java.security.MessageDigest;
 
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.crypto.codec.Utf8;
 import org.springframework.security.crypto.codec.Utf8;
 
 
 /**
 /**
@@ -36,13 +38,13 @@ final class PasswordEncoderUtils {
 	 * @param actual
 	 * @param actual
 	 * @return
 	 * @return
 	 */
 	 */
-	static boolean equals(String expected, String actual) {
+	static boolean equals(String expected, @Nullable String actual) {
 		byte[] expectedBytes = bytesUtf8(expected);
 		byte[] expectedBytes = bytesUtf8(expected);
 		byte[] actualBytes = bytesUtf8(actual);
 		byte[] actualBytes = bytesUtf8(actual);
 		return MessageDigest.isEqual(expectedBytes, actualBytes);
 		return MessageDigest.isEqual(expectedBytes, actualBytes);
 	}
 	}
 
 
-	private static byte[] bytesUtf8(String s) {
+	private static byte @Nullable [] bytesUtf8(@Nullable String s) {
 		// need to check if Utf8.encode() runs in constant time (probably not).
 		// need to check if Utf8.encode() runs in constant time (probably not).
 		// This may leak length of string.
 		// This may leak length of string.
 		return (s != null) ? Utf8.encode(s) : null;
 		return (s != null) ? Utf8.encode(s) : null;

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/password/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.password;
+
+import org.jspecify.annotations.NullMarked;

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/scrypt/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.scrypt;
+
+import org.jspecify.annotations.NullMarked;

+ 20 - 0
crypto/src/main/java/org/springframework/security/crypto/util/package-info.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2002-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.
+ * 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.
+ */
+
+@NullMarked
+package org.springframework.security.crypto.util;
+
+import org.jspecify.annotations.NullMarked;