Spring security 3 custom password encoder

Posted on: July 19th, 2012 by Spade No Comments »

Популярный фреймворк Spring security 3 всем хорош, и прежде всего с двух сторон: там есть почти все что нужно, причем по умолчанию. А если нет – можно очень легко добавить или расширить.

Для случая аутентификации, например есть много вариантов работы с паролями. Это реализуется с помощью тэга password-encoder:

    <authentication-manager>
        <authentication-provider user-service-ref="userDetailsServiceImpl">
            <password-encoder hash="md5"/>
        </authentication-provider>
    </authentication-manager>

Там можно указать тип хэша, который хранится в базе. На выбор есть md5, sha, plaintext и др. (если вы храните пароли в базе в плейн тексте, просим вас оставаться на месте – служба зачистки уже выехала за вами). В последнее время участились случаи «увода» базы хэшей и их расшифровки (даже среди крупных игроков ИТ-рынка), и потому заказчики стали обращать больше внимания на то, как хранятся данные пользователей, и насколько легко получить вход в аккаунт, если хэш пароля стал известен. Алгоритмы предлагаемые Spring security 3 по умолчанию нельзя назвать особо защищенными, но сам фреймворк нам как-бы говорит – «Не нравится? Пиши своё». Воспользуемся его предложением.

Задача – написать свой класс для создания хэша пароля. Основные требования:

  • Хэш должен быть сложно взламываемым (MD5 сразу не подходит)
  • Соль должна быть уникальна для каждого пароля

Чтобы этот класс подключался в спринг, он должен реализовывать org.springframework.security.authentication.encoding.PasswordEncoder с двумя основными функциями:

  • encodePassword – создать хеш для хранения в базе
  • isPasswordValid – сравнить хэш из базы с хэшем от введенного пароля

Алгоритм предлагается следующий:

  • Сгенерировать случайную соль
  • Сложить пароль и соль
  • Получить хеш
  • Вставить соль внутрь хэша в определенном месте

Для сравнения хэша из базы и пароля нужно:

  • Достать соль из хэша (она хранится в определенном месте)
  • Далее выполнить предыдущий алгоритм начиная с пункта 2 и сравнить хэши

Для получения качественного хэша предлагается использовать DigestUtils.sha512Hex – от apache commons. Параметры работы с солью задаются константами в классе – длинна строки при генерации и в какую позицию в хэше её нужно вставить.

Далее представим код класса:

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
 
public class PasswordEncoder implements org.springframework.security.authentication.encoding.PasswordEncoder {
 
    public static final int SALT_LENGTH = 16;
    public static final int SALT_PLACE = 73;
 
    @Override
    public String encodePassword(String rawPass, Object salt) throws DataAccessException {
        try {
            String saltStr = randomSalt();
            return encrypt(rawPass, saltStr);
        } catch (Exception e) {
            throw new DataAccessResourceFailureException("Failed to encode password.", e);
        }
    }
 
    @Override
    public boolean isPasswordValid(String encPass, String rawPass, Object salt) throws DataAccessException {
        try {
            String saltStr = extractSaltFromHash(encPass);
            return encrypt(rawPass, saltStr).equals(encPass);
        } catch (Exception e) {
            throw new DataAccessResourceFailureException("Failed to validate password.", e);
        }
    }
 
    public String encrypt(String pass, String salt) {
 
        String saltPassStr = addSaltToPassword(pass, salt);
        String hash = getSHA(saltPassStr);
        return addSaltToHash(hash, salt);
    }
 
    public String getSHA(String raw) {
        return DigestUtils.sha512Hex(raw);
    }
 
    public String randomSalt() {
        return RandomStringUtils.random(SALT_LENGTH, "abcdef0123456789");
    }
 
    public String addSaltToPassword(String password, String salt) {
        return new StringBuilder(password).append(salt).toString();
    }
 
    public String addSaltToHash(String hash, String salt) {
        String part1 = StringUtils.substring(hash, 0, SALT_PLACE);
        String part2 = StringUtils.substring(hash, SALT_PLACE);
 
        return new StringBuilder(part1).append(salt).append(part2).toString();
    }
 
    public String extractSaltFromHash(String hash) {
        return StringUtils.substring(hash, SALT_PLACE, SALT_PLACE + SALT_LENGTH);
    }
}

Обратите внимание, что соль должна состоять только из символов для выражения шестнадцатеричных значений. Потому, что SHA на выходе дает строку именно такого типа, и если символы соли будут из более широкого набора значений – её будет легче выделить.

Конечно, алгоритм шифрования может считаться надежным, только если раскрытие его инструкций мало повлияет на сложность процесса расшифровки. В данном случае, если хакер кроме базы получил доступ к коду приложения и знает где искать соль – большой пользы от этого не будет. Тем не менее это усложнение может помочь в случае, если у него есть только база. Кроме того, если соль будет одна для всех – это лишь облегчит его работу.

Само собой тут есть большое поле для самовыражения и полета фантазии:

  • можно функцию хеша вызывать циклом несколько раз подряд – это сильно увеличит надежность
  • можно соль создавать произвольного размера и хранить в произвольном месте хэша (это усложнит скорее имплементацию алгоритма, а не его качество)
  • и др.

В самом спринг конфиге мы подключаем наш класс, и говорим что именно его нужно использовать.

    <authentication-manager>
        <authentication-provider user-service-ref="userDetailsServiceImpl">
            <password-encoder ref="passwordEncoder"/>
        </authentication-provider>
    </authentication-manager>
 
    <beans:bean id="passwordEncoder"/>

Ждем ваших просьб и предложений!

Leave a Reply