diff --git a/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java b/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java index 1813953f42..67b1b9576f 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java +++ b/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java @@ -52,7 +52,9 @@ public final class PasswordEncoderFactories { *
  • MD5 - {@code new MessageDigestPasswordEncoder("MD5")}
  • *
  • noop - * {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}
  • - *
  • pbkdf2 - {@link Pbkdf2PasswordEncoder}
  • + *
  • pbkdf2 - {@link Pbkdf2PasswordEncoder#defaultsForSpringSecurity_v5_5()}
  • + *
  • pbkdf2@SpringSecurity_v5_8 - + * {@link Pbkdf2PasswordEncoder#defaultsForSpringSecurity_v5_8()}
  • *
  • scrypt - {@link SCryptPasswordEncoder#defaultsForSpringSecurity_v4_1()}
  • *
  • scrypt@SpringSecurity_v5_8 - * {@link SCryptPasswordEncoder#defaultsForSpringSecurity_v5_8()}
  • @@ -75,7 +77,8 @@ public final class PasswordEncoderFactories { encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); - encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); + encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()); + encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()); encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java index 4e5b292d4f..f26e74b1ba 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -36,8 +36,6 @@ import org.springframework.security.crypto.util.EncodingUtils; *
  • a configurable random salt value length (default is {@value #DEFAULT_SALT_LENGTH} * bytes)
  • *
  • a configurable number of iterations (default is {@value #DEFAULT_ITERATIONS})
  • - *
  • a configurable output hash width (default is {@value #DEFAULT_HASH_WIDTH} - * bits)
  • *
  • a configurable key derivation function (see {@link SecretKeyFactoryAlgorithm})
  • *
  • a configurable secret appended to the random salt (default is empty)
  • * @@ -50,72 +48,97 @@ import org.springframework.security.crypto.util.EncodingUtils; */ public class Pbkdf2PasswordEncoder implements PasswordEncoder { - private static final int DEFAULT_SALT_LENGTH = 8; + private static final int DEFAULT_SALT_LENGTH = 16; - private static final int DEFAULT_HASH_WIDTH = 256; + private static final SecretKeyFactoryAlgorithm DEFAULT_ALGORITHM = SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256; - private static final int DEFAULT_ITERATIONS = 185000; + private static final int DEFAULT_HASH_WIDTH = 256; // SHA-256 + + private static final int DEFAULT_ITERATIONS = 310000; private final BytesKeyGenerator saltGenerator; private final byte[] secret; - private final int hashWidth; - private final int iterations; - private String algorithm = SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(); + private String algorithm = DEFAULT_ALGORITHM.name(); + + private int hashWidth = DEFAULT_HASH_WIDTH; + + // @formatter:off + /* + The length of the hash should be derived from the hashing algorithm. + + For example: + SHA-1 - 160 bits (20 bytes) + SHA-256 - 256 bits (32 bytes) + SHA-512 - 512 bits (64 bytes) + + However, the original configuration for PBKDF2 was hashWidth=256 and algorithm=SHA-1, which is incorrect. + The default configuration has been updated to hashWidth=256 and algorithm=SHA-256 (see gh-10506). + In order to preserve backwards compatibility, the variable 'overrideHashWidth' has been introduced + to indicate usage of the deprecated constructor that honors the hashWidth parameter. + */ + // @formatter:on + private boolean overrideHashWidth = true; private boolean encodeHashAsBase64; /** * Constructs a PBKDF2 password encoder with no additional secret value. There will be - * a salt length of {@value #DEFAULT_SALT_LENGTH} bytes, {@value #DEFAULT_ITERATIONS} - * iterations and a hash width of {@value #DEFAULT_HASH_WIDTH} bits. The default is - * based upon aiming for .5 seconds to validate the password when this class was - * added. Users should tune password verification to their own systems. + * a salt length of 8 bytes, 185,000 iterations, SHA-1 algorithm and a hash length of + * 256 bits. The default is based upon aiming for .5 seconds to validate the password + * when this class was added. Users should tune password verification to their own + * systems. + * @deprecated Use {@link #defaultsForSpringSecurity_v5_5()} instead */ + @Deprecated public Pbkdf2PasswordEncoder() { this(""); } /** - * Constructs a standard password encoder with a secret value which is also included - * in the password hash. There will be a salt length of {@value #DEFAULT_SALT_LENGTH} - * bytes, {@value #DEFAULT_ITERATIONS} iterations and a hash width of - * {@value #DEFAULT_HASH_WIDTH} bits. + * Constructs a PBKDF2 password encoder with a secret value which is also included in + * the password hash. There will be a salt length of 8 bytes, 185,000 iterations, + * SHA-1 algorithm and a hash length of 256 bits. * @param secret the secret key used in the encoding process (should not be shared) + * @deprecated Use {@link #Pbkdf2PasswordEncoder(CharSequence, int, int, int)} instead */ + @Deprecated public Pbkdf2PasswordEncoder(CharSequence secret) { - this(secret, DEFAULT_SALT_LENGTH, DEFAULT_ITERATIONS, DEFAULT_HASH_WIDTH); + this(secret, 8); } /** - * Constructs a standard password encoder with a secret value as well as salt length. - * There will be {@value #DEFAULT_ITERATIONS} iterations and a hash width of - * {@value #DEFAULT_HASH_WIDTH} bits. + * Constructs a PBKDF2 password encoder with a secret value as well as salt length. + * There will be 185,000 iterations, SHA-1 algorithm and a hash length of 256 bits. * @param secret the secret * @param saltLength the salt length (in bytes) * @since 5.5 + * @deprecated Use {@link #Pbkdf2PasswordEncoder(CharSequence, int, int, int)} instead */ + @Deprecated public Pbkdf2PasswordEncoder(CharSequence secret, int saltLength) { - this(secret, saltLength, DEFAULT_ITERATIONS, DEFAULT_HASH_WIDTH); + this(secret, saltLength, 185000, 256); } /** - * Constructs a standard password encoder with a secret value as well as iterations - * and hash width. The salt length will be of {@value #DEFAULT_SALT_LENGTH} bytes. + * Constructs a PBKDF2 password encoder with a secret value as well as iterations and + * hash width. The salt length will be 8 bytes. * @param secret the secret * @param iterations the number of iterations. Users should aim for taking about .5 * seconds on their own system. * @param hashWidth the size of the hash (in bits) + * @deprecated Use {@link #Pbkdf2PasswordEncoder(CharSequence, int, int, int)} instead */ + @Deprecated public Pbkdf2PasswordEncoder(CharSequence secret, int iterations, int hashWidth) { - this(secret, DEFAULT_SALT_LENGTH, iterations, hashWidth); + this(secret, 8, iterations, hashWidth); } /** - * Constructs a standard password encoder with a secret value as well as salt length, + * Constructs a PBKDF2 password encoder with a secret value as well as salt length, * iterations and hash width. * @param secret the secret * @param saltLength the salt length (in bytes) @@ -123,12 +146,65 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder { * seconds on their own system. * @param hashWidth the size of the hash (in bits) * @since 5.5 + * @deprecated Use + * {@link #Pbkdf2PasswordEncoder(CharSequence, int, int, SecretKeyFactoryAlgorithm)} + * instead */ + @Deprecated public Pbkdf2PasswordEncoder(CharSequence secret, int saltLength, int iterations, int hashWidth) { this.secret = Utf8.encode(secret); this.saltGenerator = KeyGenerators.secureRandom(saltLength); this.iterations = iterations; this.hashWidth = hashWidth; + this.algorithm = SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name(); + this.overrideHashWidth = false; // Honor 'hashWidth' to preserve backwards + // compatibility + } + + /** + * Constructs a PBKDF2 password encoder with a secret value as well as salt length, + * iterations and algorithm. + * @param secret the secret + * @param saltLength the salt length (in bytes) + * @param iterations the number of iterations. Users should aim for taking about .5 + * seconds on their own system. + * @param secretKeyFactoryAlgorithm the algorithm to use + * @since 5.8 + */ + public Pbkdf2PasswordEncoder(CharSequence secret, int saltLength, int iterations, + SecretKeyFactoryAlgorithm secretKeyFactoryAlgorithm) { + this.secret = Utf8.encode(secret); + this.saltGenerator = KeyGenerators.secureRandom(saltLength); + this.iterations = iterations; + setAlgorithm(secretKeyFactoryAlgorithm); + } + + /** + * Constructs a PBKDF2 password encoder with no additional secret value. There will be + * a salt length of 8 bytes, 185,000 iterations, SHA-1 algorithm and a hash length of + * 256 bits. The default is based upon aiming for .5 seconds to validate the password + * when this class was added. Users should tune password verification to their own + * systems. + * @return the {@link Pbkdf2PasswordEncoder} + * @since 5.8 + * @deprecated Use {@link #defaultsForSpringSecurity_v5_8()} instead + */ + @Deprecated + public static Pbkdf2PasswordEncoder defaultsForSpringSecurity_v5_5() { + return new Pbkdf2PasswordEncoder("", 8, 185000, 256); + } + + /** + * Constructs a PBKDF2 password encoder with no additional secret value. There will be + * a salt length of 16 bytes, 310,000 iterations, SHA-256 algorithm and a hash length + * of 256 bits. The default is based upon aiming for .5 seconds to validate the + * password when this class was added. Users should tune password verification to + * their own systems. + * @return the {@link Pbkdf2PasswordEncoder} + * @since 5.8 + */ + public static Pbkdf2PasswordEncoder defaultsForSpringSecurity_v5_8() { + return new Pbkdf2PasswordEncoder("", DEFAULT_SALT_LENGTH, DEFAULT_ITERATIONS, DEFAULT_ALGORITHM); } /** @@ -153,6 +229,10 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder { catch (NoSuchAlgorithmException ex) { throw new IllegalArgumentException("Invalid algorithm '" + algorithmName + "'.", ex); } + if (this.overrideHashWidth) { + this.hashWidth = SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.equals(secretKeyFactoryAlgorithm) ? 160 + : SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256.equals(secretKeyFactoryAlgorithm) ? 256 : 512; + } } /** diff --git a/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java b/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java index c8c19553d2..0b2dbd4a02 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java @@ -75,6 +75,12 @@ public class PasswordEncoderFactoriesTests { assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue(); } + @Test + public void matchesWhenPbkdf2SpringSecurity_v5_8ThenWorks() { + String encodedPassword = "{pbkdf2@SpringSecurity_v5_8}fefe5120467e5d4ccff442dbb2fa86d276262d97435c0c54e5eebced51ffd144fcb05eb53fea2677216c4f3250010006"; + assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue(); + } + @Test public void matchesWhenSCryptThenWorks() { String encodedPassword = "{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc="; diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java index 1a8ea46f74..7a693d2fce 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -198,6 +198,14 @@ public class Pbkdf2PasswordEncoderTests { assertThat(this.encoderSalt16.matches(rawPassword, encodedPassword)).isTrue(); } + @Test + public void matchWhenDefaultsForSpringSecurity_v5_8ThenSuccess() { + Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + String rawPassword = "password"; + String encodedPassword = "fefe5120467e5d4ccff442dbb2fa86d276262d97435c0c54e5eebced51ffd144fcb05eb53fea2677216c4f3250010006"; + assertThat(encoder.matches(rawPassword, encodedPassword)).isTrue(); + } + /** * Used to find the iteration count that takes .5 seconds. */