Вопрос или проблема
Я пишу 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.
Полная реализация
-
Генерация соли: Соль должна быть случайной и уникальной для каждой записи. Это поможет предотвратить атаки с использованием радужных таблиц.
-
Хеширование пароля: Используя PBKDF2 для хеширования пароля с указанным количеством итераций.
-
Генерация 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");
}
}
Объяснение кода
-
Генерация соли: Используется
SecureRandom
для генерации 16-байтной соли. Соль должна храниться вместе с хешем пароля и использоваться каждый раз при аутентификации. -
Генерация ключа: Метод
generateKey
создает экземплярSecretKeyFactory
с алгоритмомPBKDF2WithHmacSHA256
, используетPBEKeySpec
для хеширования пароля вместе с солью, и задает количество итераций. -
Кодирование: Соль и ключ кодируются в формат Base64 для удобства хранения (например, в базе данных).
Заключение
Обратите внимание, что важно иметь уникальную соль для каждого пользователя, чтобы обеспечить безопасность. Так как одно и то же значение пароля с одинаковой солью приведет к одному и тому же хешу, это открывает возможность для атак. Каждый пользователь должен иметь свою уникальную соль, которую можно хранить вместе с хешем пароля.
Также, помимо использования PBKDF2, вы можете рассмотреть и другие методы, например Argon2, который обеспечивает ещё большую устойчивость к атакам. Но если вы работаете с PBKDF2, соблюдение правил безопасной реализации делает вашу систему гораздо более защищённой.