Користувацький пост про всі алгоритми кодування пароля, які є в Spring Security. Я написав його для того, щоб мати методичку, якщо мені доведеться підбирати власноруч кодувальник паролю, адже схожих постів я не знайшов.
PasswordEncoder
PasswordEncoder - це інтерфейс в Spring Security, який відповідає за кодування та порівняння паролів, його використовує DaoAuthenticationProvider для того, щоб автентифікувати юзера за паролем.
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
Основні імплементації
AbstractPasswordEncoder
Це не те, щоб повноцінна реалізація, він надає частинку реалізації, загальні методи, які можна використати у власній реалізації, щоб не вигадувати велосипед.
public String encode(CharSequence rawPassword) {
byte[] salt = this.saltGenerator.generateKey();
byte[] encoded = this.encodeAndConcatenate(rawPassword, salt);
return String.valueOf(Hex.encode(encoded));
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
byte[] digested = Hex.decode(encodedPassword);
byte[] salt = EncodingUtils.subArray(digested, 0, this.saltGenerator.getKeyLength());
return matches(digested, this.encodeAndConcatenate(rawPassword, salt));
}
BCryptPasswordEncoder
Про нього часто розказують в туторіалах в першу чергу, бо BCryptPasswordEncoder є дефолтним енкодером в Spring Security. Цей екодер використовує bcrypt алгоритм.
BCrypt - це алгоритм кодування паролів, який використовується для зберігання паролів у безпечній формі. Основною його особливістю є можливість конфігурувати затрати для обчислення функції, які можна регулювати за допомогою параметру "cost factor". Це робить атаки "брутфорс" або "dictionary" над паролями набагато більш витратними в часі і ресурсах.
По дефолту кост фактор / раундс рівний 10
По дефолту сила алгоритму рівна -1
public BCryptPasswordEncoder() {
this(-1);
}
Сила алгоритму використовується при генерації сілі:
private String getSalt() {
return this.random != null ? BCrypt.gensalt(this.version.getVersion(),
this.strength, this.random) :
BCrypt.gensalt(this.version.getVersion(), this.strength);
}
Argon2PasswordEncoder
Argon2 це доволі новий алгоритм для кодування паролю, він був розроблений в 2015-му році.
Argon2 вважається більш стійким до атак, ніж BCrypt, оскільки він розроблений з урахуванням останніх відомостей про методи злому паролів. Він використовує більш складні методи оптимізації, щоб ускладнити атаки "брутфорс" і "dictionary".
Argon2 є параметризованим алгоритмом, можна налаштувати різні параметри: пам'ять, час, паралельність
public Argon2PasswordEncoder(int saltLength, int hashLength, int parallelism, int memory, int iterations) {
this.hashLength = hashLength;
this.parallelism = parallelism;
this.memory = memory;
this.iterations = iterations;
this.saltGenerator = KeyGenerators.secureRandom(saltLength);
}
Дефолтні значення Argon2 для Spring Security
@Deprecated
public static Argon2PasswordEncoder defaultsForSpringSecurity_v5_2() {
return new Argon2PasswordEncoder(16, 32, 1, 4096, 3);
}
public static Argon2PasswordEncoder defaultsForSpringSecurity_v5_8() {
return new Argon2PasswordEncoder(16, 32, 1, 16384, 2);
}
Argon2 є відносно новим алгоритмом кодування паролів, який був розроблений спеціально для забезпечення безпеки паролів у сучасних застосунках.
У зв'язку з великими обчислювальними витратами Argon2 може займати більше часу на кодування паролів порівняно з BCrypt. Це може бути важливо при обробці великого обсягу запитів аутентифікації в масштабованих системах.
Pbkdf2PasswordEncoder
Цей енкодер використовує алгоритм PBKDF2 для кодування паролів. PBKDF2 (Password-Based Key Derivation Function 2) є одним із найбільш безпечних алгоритмів кодування паролів, рекомендованим NIST.
Є декілька можливих алгоритмів, які сетяться публічним методом.
public static enum SecretKeyFactoryAlgorithm {
PBKDF2WithHmacSHA1,
PBKDF2WithHmacSHA256,
PBKDF2WithHmacSHA512;
private SecretKeyFactoryAlgorithm() {
}
}
І один дефолтний
static {
DEFAULT_ALGORITHM = Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256;
}
Конструктор
public Pbkdf2PasswordEncoder(CharSequence secret, int saltLength, int iterations, SecretKeyFactoryAlgorithm secretKeyFactoryAlgorithm) {
this.algorithm = DEFAULT_ALGORITHM.name();
this.hashWidth = 256;
this.overrideHashWidth = true;
this.secret = Utf8.encode(secret);
this.saltGenerator = KeyGenerators.secureRandom(saltLength);
this.iterations = iterations;
this.setAlgorithm(secretKeyFactoryAlgorithm);
}
Дефолтні спрінг конфігурації
@Deprecated
public static Pbkdf2PasswordEncoder defaultsForSpringSecurity_v5_5() {
return new Pbkdf2PasswordEncoder("", 8, 185000, 256);
}
public static Pbkdf2PasswordEncoder defaultsForSpringSecurity_v5_8() {
return new Pbkdf2PasswordEncoder("", 16, 310000, DEFAULT_ALGORITHM);
}
Pbkdf2PasswordEncoder
є гарним вибором для кодування паролів в Spring Security, особливо якщо безпека є важливим аспектом для вашого додатку.
SCryptPasswordEncoder
Цей енкодер використовує алгоритм SCrypt для кодування паролів. SCrypt є алгоритмом кодування, який схожий на PBKDF2, але зазвичай вважається більш стійким до атак, особливо до атак з використанням спеціалізованих апаратних засобів.
Дефолтні характеристики
private static final int DEFAULT_CPU_COST = 65536;
private static final int DEFAULT_MEMORY_COST = 8;
private static final int DEFAULT_PARALLELISM = 1;
private static final int DEFAULT_KEY_LENGTH = 32;
private static final int DEFAULT_SALT_LENGTH = 16;
Конструктор
public SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength) {
Дефолтні конфіги спрінга
@Deprecated
public static SCryptPasswordEncoder defaultsForSpringSecurity_v4_1() {
return new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
}
public static SCryptPasswordEncoder defaultsForSpringSecurity_v5_8() {
return new SCryptPasswordEncoder(65536, 8, 1, 32, 16);
}
Швидкість обчислення однієї операції scrypt на процесорі загального призначення становить близько 100 мілісекунд при налаштуванні на використання 32 МБ пам'яті. При налаштуванні на тривалість операції в 1 мілісекунду використовується дуже мало пам'яті і алгоритм стає слабшим алгоритму bcrypt, налаштованого на порівнянну швидкість.
DelegatingPasswordEncoder
Цей клас є внутріщнім механізмом в Spring Security, який дозволяє використовувати різне кодування для різних юзерів / типів паролів.
Є три основні параметри: Мапа, яка є конфігурацією енкодерів, passwordEncoderForEncode який сетиться в конструкторі і idForEncode, яке теж просто сетиться в конструкторі.
private final String idForEncode;
private final PasswordEncoder passwordEncoderForEncode;
private final Map<String, PasswordEncoder> idToPasswordEncoder;
this.idForEncode = idForEncode;
this.passwordEncoderForEncode =
(PasswordEncoder)idToPasswordEncoder.get(idForEncode);
Приклад написання DelegatingPasswordEncoder біна.
@Bean
public PasswordEncoder passwordEncoder() {
String idForEncode = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new Pbkdf2PasswordEncoder());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
Порівняння:
Ці алгоритми сильно залежать від конфігурації, той самий SCrypt можна налаштувати, щоб одна його операція виконувалась 1ms, але він стане слабшим алгоритму bcrypt.
BCryptPasswordEncoder не має паралелізму в кодуванні, як Argon2, чи SCrypt.
Щодо шкидкості Argon2, SCrypt, PBKDF2 важко судити, бо вони всі параметризуються використанням ресурсів. Хіба що, варто вказати, що PBKDF2Encoder не має вбудованої паралелізації.
Застарілі
Якщо коротко то застаріло все, що трималось на Java Digest + NoOpPasswordEncoder, щоб змушувати користувачів кодувати паролі у буь-якому випадку.
NoOpPasswordEncoder
Цей енкодер просто перетворює CharSequence пароля в стрічку, він не кодує пароль, але являється імплементацією PasswordEncoder.
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
LdapShaPasswordEncoder
Зрозуміло, що використовується для LDAP і використовує SHA кодування, нічого цікавого.
Довжина SHA
private static final int SHA_LENGTH = 20;
Md4PasswordEncoder
Це ще один алгоритм кодуваня MD4, алгоритм був розроблений у 90-х і вважається вразливим.
MD4 не рекомендується для захисту паролів через свою низьку стійкість до атак. Він вразливий до багатьох видів атак, включаючи атаки з перебором, атаки на витягнення, атаки на колізії та інші.
Має один параметр
public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {}
MessageDigestPasswordEncoder
Це узагальнений енкодер, який підтьримує будь-який MessageDigest алгоритм з джави, який в нього запхають MD4, MD5, SHA, все, що підтримує Java MessageDigest, все застаріло.
private Digester digester;
public MessageDigestPasswordEncoder(String algorithm) {
this.digester = new Digester(algorithm, 1);
}
StandardPasswordEncoder
Застарілий і більше не стандартний (Тепер BCrypt). Він створює хеш-код паролю за допомогою Digest алгоритму з сіллю (salt) та ітераціями (iterations).
private StandardPasswordEncoder(String algorithm, CharSequence secret) {
this.digester = new Digester(algorithm, 1024);
this.secret = Utf8.encode(secret);
this.saltGenerator = KeyGenerators.secureRandom();
}
Все, що використовує Digester алгоритми застаріло.
Використовує SHA-256
Дефолтна кількість ітерацій:
private static final int DEFAULT_ITERATIONS = 1024;
Вбудовані PasswordEncoder
AuthenticationConfiguration.LazyPasswordEncoder HttpSecurityConfiguration.LazyPasswordEncoder
В самому security є ще два LazyPasswordEncoder.
Це дублікація коду, обидва класи потрібні для лінивої підгрузки паролів, коли вони потрібні.
DelagatingPasswordEncoder.UnmappedIdPasswordEncoder
Це сутність, яка потрібна для заглушки незамаплених id в DelagatingPasswordEncoder. Всі методи повертають ексепшини.
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
private UnmappedIdPasswordEncoder() {
}
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
String id = DelegatingPasswordEncoder.this.extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}