Статьи

Безопасность GlassFish JDBC с солеными паролями на MySQL

Одной из самых успешных публикаций в этом блоге является моя статья о настройке JDBC Security Realm с аутентификацией на основе форм в GlassFish. Некоторые комментарии к этому сообщению заставили меня осознать, что нужно сделать еще многое, чтобы обеспечить безопасность, как и должно быть.

Безопасность из коробки

Картинка: TheKenChan ( CC BY-NC 2.0 )

GlassFish уже поставляется с JDBC Realm GlassFish . Все, что вам нужно сделать, это инициализировать базу данных и получить правильную конфигурацию безопасности, и все готово. Среди стандартной конфигурации у вас есть возможность определить алгоритм дайджеста (включая кодирование и кодировку). Алгоритм дайджеста может быть любым поддерживаемым JDK MessageDigest (MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512). Сравните мой пост JDBC Security Realm для полной настройки.

Что слабое или отсутствует?

Решение «из коробки» идет очень тривиально. Он просто хэширует пароль. Есть много способов очень быстро восстановить пароли из простых хешей. Самый простой способ взломать хеш — попытаться угадать пароль, хэшировать каждую догадку и проверять, соответствует ли хеш догадки взломанному хешу. Если хэши равны, значит, пароль. Двумя наиболее распространенными способами угадывания паролей являются атаки по словарю и атаки методом перебора. Также очень широко известны таблицы поиска. Они являются эффективным методом быстрого взлома множества хэшей одного типа. Основная идея состоит в том, чтобы предварительно вычислить хэши паролей в словаре паролей и сохранить их и соответствующий им пароль в структуре данных таблицы поиска. Но мы еще не закончили. Вы также найдете то, что называется таблицами обратного просмотра. Эта атака позволяет злоумышленнику применить атаку по словарю или методом грубой силы ко многим хэшам одновременно, без необходимости предварительно вычислять таблицу поиска. И последнее, но не менее важное — атака Радужных Столов. Они похожи на таблицы поиска, за исключением того, что они жертвуют скоростью взлома хеша, чтобы сделать таблицы поиска меньше. Очень впечатляющий список подходов. Очевидно, что это не отвечает моей личной потребности в защите паролей.

Добавляем немного соли

Вышеуказанные подходы работают из-за того, что каждый пароль хэшируется одинаково. Каждый раз, когда вы запускаете пароль через безопасную хеш-функцию, он выдает точно такой же результат. Один из способов предотвратить это — добавить немного соли. Добавление или добавление случайной строки к паролю перед его хэшированием решит эту проблему. Эта случайная строка называется «солью». Помните, что повторное использование соли для всех паролей небезопасно. Вы все еще можете использовать радужные таблицы или словарные атаки, чтобы взломать их. Таким образом, вы должны рандомизировать соль для каждого пароля и хранить ее рядом с хешированным паролем. И это нужно менять каждый раз, когда пользователь обновляет свой пароль. Короткое предложение о длине. Соли не должны быть слишком короткими. Для наиболее эффективной длины будет такой же размер, как хеш пароля. Если вы используете SHA512 (512/8 бит = 64 байта), вы должны выбрать соль длиной не менее 64 случайных байтов.

Препараты

Мы явно покидаем стандартные функции JDBCRealm сейчас. Что означает, что мы должны реализовать нашу собственную область безопасности. Давайте теперь будем называть это UserRealm. Давайте начнем с той же настройки, что и для JDBCRealm. База данных MySQL со схемой «jdbcrealmdb». Единственная разница здесь, мы готовим сохранить соль с каждым паролем.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
USE jdbcrealmdb;
CREATE TABLE `jdbcrealmdb`.`users` (
`username` varchar(255) NOT NULL,
`salt` varchar(255) NOT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 
CREATE TABLE `jdbcrealmdb`.`groups` (
`username` varchar(255) DEFAULT NULL,
`groupname` varchar(255) DEFAULT NULL)
ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE INDEX groups_users_FK1 ON groups(username ASC);

Теперь мы реализуем основную сферу. Следующий код просто показывает обязательные члены. Я собираюсь сделать источник доступным в течение следующих дней. До сегодняшнего дня этот пост — все, что доступно для вас.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UserRealm extends AppservRealm {
/**
* Init realm from properties
*/
protected void init(Properties props)
/**
* Get JAASContext
*/
public String getJAASContext()
/**
* Get AuthType
*/
public String getAuthType()
/**
* Get DB Connection
*/
private Connection getConnection()
/**
* Close Connection
*/
private void closeConnection(Connection cn)
/**
* Close prepared statement
*/
private void closeStatement(PreparedStatement st)
/**
* Make the compiler happy.
*/
public Enumeration getGroupNames(String string)
/**
* Authenticate the user
*/
public String[] authenticate(String userId, String password)
}

Но самая важная часть здесь отсутствует.

Настройка некоторых тестов

Я не совсем из тех, кто занимается тест-драйвом, но в данном случае это действительно имеет смысл. Потому что область, которую я собираюсь реализовать, не поддерживает управление пользователями через консоль администратора GlassFish. Поэтому основным требованием является наличие готовой базы данных со всеми пользователями, паролями и солями. Поехали. Добавьте sql-maven-plugin и позвольте ему создавать таблицы во время фазы тестовой компиляции.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<plugin>
               <groupId>org.codehaus.mojo</groupId>
               <artifactId>sql-maven-plugin</artifactId>
               <version>1.3</version>
               <dependencies>
                   <dependency>
                       <groupId>mysql</groupId>
                       <artifactId>mysql-connector-java</artifactId>
                       <version>5.1.18</version>
                   </dependency>
               </dependencies>
               <configuration>
                   <driver>${driver}</driver>
                   <url>${url}</url>
                   <username>${username}</username>
                   <password>${password}</password>
                   <skip>${maven.test.skip}</skip>
                   <srcFiles>
                       <srcFile>src/test/data/drop-and-create-table.sql</srcFile>
                   </srcFiles>
               </configuration>
               <executions>
                   <execution>
                       <id>create-table</id>
                       <phase>test-compile</phase>
                       <goals>
                           <goal>execute</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>

Вы можете использовать магию db-unit для вставки тестовых данных в вашу базу данных или сделать это в своих тестовых примерах. Я решил пойти по этому пути. Сначала давайте поместим все соответствующие материалы JDBC в отдельное место под названием SecurityStore. Нам в основном нужно три метода. Добавьте пользователя, получите соль для пользователя и подтвердите пользователя.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private final static String ADD_USER = "INSERT INTO users VALUES(?,?,?);";
    private final static String SALT_FOR_USER = "SELECT salt FROM users u WHERE username = ?;";
    private final static String VERIFY_USER = "SELECT username FROM users u WHERE username = ? AND password = ?;";
//...
public void addUser(String name, String salt, String password) {
        try {
            PreparedStatement pstm = con.prepareStatement(ADD_USER);
            pstm.setString(1, name);
            pstm.setString(2, salt);
            pstm.setString(3, password);
            pstm.executeUpdate();
        } catch (SQLException ex) {
            LOGGER.log(Level.SEVERE, "Create User failed!", ex);
        }
    }
 
    public String getSaltForUser(String name) {
        String salt = null;
        try {
            PreparedStatement pstm = con.prepareStatement(SALT_FOR_USER);
            pstm.setString(1, name);
            ResultSet rs = pstm.executeQuery();
 
            if (rs.next()) {
                salt = rs.getString(1);
            }
 
        } catch (SQLException ex) {
            LOGGER.log(Level.SEVERE, "User not found!", ex);
        }
        return salt;
    }
 
    public boolean validateUser(String name, String password) {
        try {
            PreparedStatement pstm = con.prepareStatement(VERIFY_USER);
            pstm.setString(1, name);
            pstm.setString(2, password);
            ResultSet rs = pstm.executeQuery();
            if (rs.next()) {
                return true;
            }
        } catch (SQLException ex) {
            LOGGER.log(Level.SEVERE, "User validation failed!", ex);
        }
        return false;
    }

Чтобы не реализовывать слишком много здесь, я решил использовать два отдельных конструктора:

1
2
public SecurityStore(String dataSource)
public SecurityStore(String user, String passwd)

Так что это будет работать как с сервером приложений, так и с моими локальными тестами. Далее идет действительный пароль и логика соли.

Работа с паролями, хэшами и солями

Вот что я придумал:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Password {
 
    private SecureRandom random;
    private static final String CHARSET = "UTF-8";
    private static final String ENCRYPTION_ALGORITHM = "SHA-512";
    private BASE64Decoder decoder = new BASE64Decoder();
    private BASE64Encoder encoder = new BASE64Encoder();
 
    public byte[] getSalt(int length) {
        random = new SecureRandom();
        byte bytes[] = new byte[length];
        random.nextBytes(bytes);
        return bytes;
    }
 
    public byte[] hashWithSalt(String password, byte[] salt) {
        byte[] hash = null;
        try {
            byte[] bytesOfMessage = password.getBytes(CHARSET);
            MessageDigest md;
            md = MessageDigest.getInstance(ENCRYPTION_ALGORITHM);
            md.reset();
            md.update(salt);
            md.update(bytesOfMessage);
            hash = md.digest();
 
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException ex) {
            Logger.getLogger(Password.class.getName()).log(Level.SEVERE, "Encoding Problem", ex);
        }
        return hash;
    }
 
    public String base64FromBytes(byte[] text) {
        return encoder.encode(text);
    }
 
    public byte[] bytesFrombase64(String text) {
        byte[] textBytes = null;
        try {
            textBytes = decoder.decodeBuffer(text);
        } catch (IOException ex) {
            Logger.getLogger(Password.class.getName()).log(Level.SEVERE, "Encoding failed!", ex);
        }
        return textBytes;
    }
}

Довольно легко, правда? Если честно: работа с байтом [] может быть лучше скрыта, но я подумал, что вам будет легче понять, что здесь происходит. Метод salt () возвращает безопасную случайную соль заданной длины. Метод hashWithSalt () помещает все в один хешированный пароль SHA-512.

Слово об окончаниях

Я решил кодировать его с помощью Base64, и я использую собственный API (sun.misc.BASE64Decoder, Encoder). Вы должны подумать об использовании Apache Commons здесь. Но это был самый простой способ сделать это. Другой подход состоит в том, чтобы просто HEX кодировать (с нуля) все. Разница между Base64 и HEX на самом деле заключается только в том, как представлены байты. HEX — это еще один способ сказать «Base16». HEX будет принимать два символа для каждого байта — Base64 использует 4 символа для каждых 3 байтов, поэтому он более эффективен, чем шестнадцатеричный. Предполагая, что вы используете UTF-8 для кодирования XML-документа, для файла 100K потребуется 200 КБ в шестнадцатеричном формате или 133 КБ в Base64.

И, наконец, отсутствующий метод в UserRealm

Самая последняя часть этого длинного поста — метод authenticate в классе UserRealm.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * Authenticates a user against GlassFish
 *
 * @param name The user name
 * @param givenPwd The password to check
 * @return String[] of the groups a user belongs to.
 * @throws Exception
 */
public String[] authenticate(String name, String givenPwd) throws Exception {
    SecurityStore store = new SecurityStore(dataSource);
    // attempting to read the users-salt
    String salt = store.getSaltForUser(name);
 
    // Defaulting to a failed login by setting null
    String[] result = null;
 
    if (salt != null) {
        Password pwd = new Password();
        // get the byte[] from the salt
        byte[] saltBytes = pwd.bytesFrombase64(salt);
        // hash password and salt
        byte[] passwordBytes = pwd.hashWithSalt(givenPwd, saltBytes);
        // Base64 encode to String
        String password = pwd.base64FromBytes(passwordBytes);
        _logger.log(Level.FINE, "PWD Generated {0}", password);
        // validate password with the db
        if (store.validateUser(name, password)) {
            result[0] = "ValidUser";
        }
    }
    return result;
}

Это все, что нужно сделать здесь. Если у нас есть соль для данного имени пользователя, мы генерируем хешированный пароль, который мы будем проверять по сравнению с тем, который мы имеем в базе данных. GetSaltForUser () также является нашей неявной проверкой существования пользователя.

Создание паролей еще сложнее: медленные хэш-функции

Безопасность не будет называться безопасностью, если они не добавят к ней больше. Таким образом, соленые пароли намного лучше, чем просто хэшированные, но все же, вероятно, их недостаточно, потому что они по-прежнему допускают атаки методом «грубой силы» или словаря на любой отдельный хэш. Но вы можете добавить больше защиты. Ключевое слово — растяжение ключа . Также известен как медленные хэш-функции. Идея здесь состоит в том, чтобы сделать вычисления достаточно медленными, чтобы больше не допускать атак на CPU / GPU. Он реализован с использованием специальной хэш-функции с интенсивным использованием процессора. PBKDF2 (функция получения ключа на основе пароля 2) является одним из них. Вы можете использовать его по-разному, но одно предупреждение: никогда не пытайтесь делать это самостоятельно. Используйте одну из протестированных и предоставленных реализаций, таких как PBKDF2WithHmacSHA1 из JDK или PKCS5S2ParametersGenerator из библиотеки Bouncycastle. Пример может выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
public byte[] hashWithSlowsalt(String password, byte[] salt) {
    SecretKeyFactory factory;
    Key key = null;
    try {
        factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec keyspec = new PBEKeySpec(password.toCharArray(), salt, 1000, 512);
        key = factory.generateSecret(keyspec);
    } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
        Logger.getLogger(Password.class.getName()).log(Level.SEVERE, null, ex);
    }
    return key.getEncoded();
}

Зачем все это?

Мы слышим о паролях и утечках из базы данных пользователей. Каждый день. Некоторые крупные сайты пострадали, и разработчик, в основном, должен обеспечить подходящую безопасность для своих пользователей. Знание того, где и как настраивать, может быть затруднено, и честное использование предоставленных функций оставило вас с неправильным чувством комфорта. Не прекращайте узнавать о функциях безопасности и следите за возможными проблемами. Я лично хотел бы, чтобы GlassFish предоставил более полный набор областей по умолчанию для пользователей, с которыми можно работать. Но пока это не так, мой блог — единственный способ направить вас в правильном направлении. Надеюсь, вам понравилось!

Ссылка: GlassFish JDBC Security с солеными паролями на MySQL от нашего партнера по JCG Маркуса Эйзела (Markus Eisele) из блога Enterprise Software Development с Java .