Использование PBKDF2 для реализации хеширования и генерации ключа AES

Вопрос или проблема

Я пишу Java-приложение, которое должно локально аутентифицировать пользователя с помощью пароля, а затем использовать этот пароль для генерации ключа AES-256 для шифрования/дешифрования локальных файлов.

Я понимаю принципы, стоящие за всем этим, и насколько важен правильный выбор алгоритма, количество раундов хеширования и генерация крипто-случайной соли. С учетом этого я использую алгоритм PBKDF2WithHmacSHA256, поддерживаемый в Java 8, 16-байтное значение соли, сгенерированное с помощью SecureRandom в Java, и 250 000 раундов хеширования. Мой вопрос заключается в реализации, ниже представлена (упрощенная) версия того, как я генерирую хеш и ключ пользователя. Код был сокращен ради упрощения поста, и значения были захардкожены для упрощения.

int iterations = 250000;
String password = "password";
String salt = "salt";

SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
char[] passwordChars = password.toCharArray();
KeySpec spec = new PBEKeySpec(passwordChars, salt.getBytes(), iterations, 256);
SecretKey key = factory.generateSecret(spec);

byte[] passwordHash = key.getEncoded();

SecretKey secret = new SecretKeySpec(key.getEncoded(), "AES");

Этот код основан на конкатенации нескольких различных проектов с открытым исходным кодом на Java, через которые я прошел, которые также используют алгоритм PBKDF2 для хеширования паролей, генерации ключей AES или и того, и другого.

Мой вопрос: является ли это действительно безопасным? У меня есть ощущение, что использование одного и того же значения SecretKey “key” для генерации SecretKey “secret” и генерации хеша является некорректным. Если это так, может кто-то посоветовать правильный метод использования алгоритма PBKDF2WithHmacSHA512 для генерации хеша пароля и извлечения ключа AES?

Чтобы обеспечить безопасность, соль должна быть случайной и прикреплена к хешированному паролю. Обычно я делаю это, кодируя значение, которое я храню в BLOB, как фиксированный набор байтов, начинающийся с соли (IV), за которым следует хешированный пароль, и парсю это соответствующим образом. Возможны варианты, но техника в общем такая же, где соль является случайной для каждой записи в таблице. Это предотвращает использование радужных таблиц, так как в этом случае нужно будет учитывать соль + хеширование (это и стало причиной утечки данных несколько лет назад с LinkedIn и проблемы с паролями Windows NT).

Что касается данных AES, я просто добавляю еще одну случайную соль (это можно сделать в CryptoJS) или дополнительное хеширование SHA512, или и то, и другое, чтобы добавить немного больше вычислений к финальному дешифрованию. Но это не добавляет многого, поскольку секретный ключ симметричный, если данные нужно просматривать с того же устройства, и как только устройство скомпрометировано, это вопрос выполнения ключа, который хранится с зашифрованными данными. Вы только будете это делать, чтобы убедиться, что есть шифрование “на хранении”.

Это мой эквивалентный тест с использованием случайных данных повсюду в Crypto JS

test("случайная соль", () => {
  const salt = lib.WordArray.random(16);
  const password = "что-тоСекретное";
  const pb = PBKDF2(password, salt, {});
  const theKey = pb.toString();
  const secretData = {
    theMessage: lib.WordArray.random(16).toString(),
  };
  const secretMessage = JSON.stringify(secretData);
  const iv = lib.WordArray.random(16);
  const encrypted = AES.encrypt(secretMessage, theKey, {
    iv,
  }).toString();

  const pb2 = PBKDF2(password, salt, {});
  const theKey2 = pb2.toString();
  const decrypted = AES.decrypt(encrypted, theKey2, { iv }).toString(enc.Utf8);
  expect(decrypted).toBe(secretMessage);
  expect(JSON.parse(decrypted)).toStrictEqual(secretData);
});

Ответ или решение

Чтобы реализовать безопасную аутентификацию пользователя и генерирование AES-256 ключа с использованием алгоритма PBKDF2, необходимо учитывать несколько важных аспектов, таких как использование уникальной соли для каждого пользователя, количество итераций и правильное извлечение ключа. Давайте разберем, как правильно реализовать данную задачу на языке Java.

Полная реализация

  1. Генерация соли: Соль должна быть случайной и уникальной для каждой записи. Это поможет предотвратить атаки с использованием радужных таблиц.

  2. Хеширование пароля: Используя PBKDF2 для хеширования пароля с указанным количеством итераций.

  3. Генерация AES-ключа: Извлечение AES-ключа из результата хеширования пароля.

Пример кода

Вот пример, как это можно реализовать на Java:

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class PasswordEncryption {

    public static void main(String[] args) throws Exception {
        String password = "password"; // Укажите пароль
        byte[] salt = new byte[16]; // Генерация случайной соли
        SecureRandom random = new SecureRandom();
        random.nextBytes(salt);

        // Генерация AES-256 ключа
        int iterations = 250000; // Количество итераций
        SecretKey secretKey = generateKey(password.toCharArray(), salt, iterations);

        // Кодируем соль и ключ в Base64 для хранения
        String encodedSalt = Base64.getEncoder().encodeToString(salt);
        String encodedKey = Base64.getEncoder().encodeToString(secretKey.getEncoded());

        System.out.println("Соль: " + encodedSalt);
        System.out.println("AES-256 ключ: " + encodedKey);
    }

    public static SecretKey generateKey(char[] password, byte[] salt, int iterations) throws Exception {
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, 256);
        SecretKey key = factory.generateSecret(spec);

        // Возвращаем ключ для AES
        return new SecretKeySpec(key.getEncoded(), "AES");
    }
}

Объяснение кода

  1. Генерация соли: Используется SecureRandom для генерации 16-байтной соли. Соль должна храниться вместе с хешем пароля и использоваться каждый раз при аутентификации.

  2. Генерация ключа: Метод generateKey создает экземпляр SecretKeyFactory с алгоритмом PBKDF2WithHmacSHA256, использует PBEKeySpec для хеширования пароля вместе с солью, и задает количество итераций.

  3. Кодирование: Соль и ключ кодируются в формат Base64 для удобства хранения (например, в базе данных).

Заключение

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

Также, помимо использования PBKDF2, вы можете рассмотреть и другие методы, например Argon2, который обеспечивает ещё большую устойчивость к атакам. Но если вы работаете с PBKDF2, соблюдение правил безопасной реализации делает вашу систему гораздо более защищённой.

Оцените материал
Добавить комментарий

Капча загружается...