PBKDF2 с bouncycastle в Java
Я пытаюсь безопасно хранить пароль в базе данных, и для этого я решил сохранить хэш, сгенерированный с помощью функции PBKDF2. Я хочу сделать это, используя богатую библиотеку замков, но я не знаю, почему я не могу заставить ее работать, используя интерфейс JCE...
Проблема в том, что генерация хэша в трех разных режимах:
1. используя секретный ключ PBKDF2WithHmacSHA1 factory, предоставленный солнцем
2. используя надувной замок api напрямую
3. Использование бодрящего замка через JCE
приводит к двум различным значениям: один общий для первых двух и один для третьего.
Вот мой код:
//Mode 1
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec keyspec = new PBEKeySpec("password".toCharArray(), salt, 1000, 128);
Key key = factory.generateSecret(keyspec);
System.out.println(key.getClass().getName());
System.out.println(Arrays.toString(key.getEncoded()));
//Mode 2
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator();
generator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(("password").toCharArray()), salt, 1000);
KeyParameter params = (KeyParameter)generator.generateDerivedParameters(128);
System.out.println(Arrays.toString(params.getKey()));
//Mode 3
SecretKeyFactory factorybc = SecretKeyFactory.getInstance("PBEWITHHMACSHA1", "BC");
KeySpec keyspecbc = new PBEKeySpec("password".toCharArray(), salt, 1000, 128);
Key keybc = factorybc.generateSecret(keyspecbc);
System.out.println(keybc.getClass().getName());
System.out.println(Arrays.toString(keybc.getEncoded()));
System.out.println(keybc.getAlgorithm());
Я знаю, что PBKDF2 реализован с использованием HMAC SHA1, поэтому я выбрал алгоритм в последнем методе "PBEWITHHMACSHA1", который я взял с помощью javas docs от bouncy.
Вывод следующий:
com.sun.crypto.provider.SunJCE_ae
[-53, 29, 113, -110, -25, 76, 115, -127, -64, 74, -63, 102, 75, 81, -21, 74]
[-53, 29, 113, -110, -25, 76, 115, -127, -64, 74, -63, 102, 75, 81, -21, 74]
org.bouncycastle.jce.provider.JCEPBEKey
[14, -47, -87, -16, -117, -31, 91, -121, 90, -68, -82, -31, -27, 5, -93, -67, 30, -34, -64, -40]
PBEwithHmacSHA
Любые идеи?
Ответы
Ответ 1
Вкратце, причина разницы заключается в том, что алгоритм PBKDF2 в режимах # 1 и # 2 использует схему PKCS # 5 v2 2 (PKCS5S2) для генерации итеративного ключа, но поставщик BouncyCastle для "PBEWITHHMACSHA1" в режиме № 3 использует вместо этого используется алгоритм PKCS # 12 v1 (PKCS12). Это совершенно разные алгоритмы генерации ключей, поэтому вы получаете разные результаты.
Подробнее о том, почему это так, и почему вы получаете результаты разного размера, объясняется ниже.
Во-первых, когда вы создаете JCE KeySpec, параметр keyLength выражает "предпочтение" провайдеру, какой размер ключа вы хотите. Из документов API:
Примечание. Это используется, чтобы указать предпочтение по длине ключа для шифров с переменным ключом. Фактический размер ключа зависит от реализации каждого поставщика.
Поставщики Bouncy Castle, похоже, не уважают этот параметр, судя по источнику JCEPBEKey, поэтому вы должны ожидать получить 160-битный ключ от любой поставщик BC, который использует SHA-1 при использовании JCE API.
Вы можете подтвердить это, запрограммировав доступ к методу getKeySize()
в возвращаемой переменной keybc
в вашем тестовом коде:
Key keybc = factorybc.generateSecret(keyspecbc);
// ...
Method getKeySize = JCEPBEKey.class.getDeclaredMethod("getKeySize");
getKeySize.setAccessible(true);
System.out.println(getKeySize.invoke(keybc)); // prints '160'
Теперь, чтобы понять, что соответствует провайдеру "PBEWITHHMACSHA1", вы можете найти в источник для BouncyCastleProvider:
put("SecretKeyFactory.PBEWITHHMACSHA1",
"org.bouncycastle.jce.provider.JCESecretKeyFactory$PBEWithSHA");
И реализация JCESecretKeyFactory.PBEWithSHA выглядит следующим образом:
public static class PBEWithSHA
extends PBEKeyFactory
{
public PBEWithSHA()
{
super("PBEwithHmacSHA", null, false, PKCS12, SHA1, 160, 0);
}
}
Вы можете видеть выше, что этот ключ factory использует алгоритм PKCS # 12 v1 (PKCS12) для генерации итеративного ключа. Но алгоритм PBKDF2, который вы хотите использовать для хеширования паролей, вместо этого использует схему PKCS # 5 v2 2 (PKCS5S2). Вот почему вы получаете разные результаты.
Я быстро просмотрел провайдеры JCE, зарегистрированные в BouncyCastleProvider
, но не видел никаких алгоритмов генерации ключей, которые использовали PKCS5S2 вообще, не говоря уже о том, который также использует его с HMAC-SHA-1.
Итак, я полагаю, что вы застряли в использовании реализации Sun (режим № 1 выше) и теряете переносимость на других JVM или используете классы Bouncy Castle напрямую (режим № 2 выше) и требуете библиотеки BC во время выполнения.
В любом случае вам, вероятно, следует переключиться на 160-битные ключи, поэтому вы не будете без необходимости обрезать созданный хэш SHA-1.
Ответ 2
Я нашел метод BC Crypto-Only (фактически из пакета cms BC), который работает для создания кодировки пароля на основе UTF-8. Таким образом, я могу генерировать вывод KDF, совместимый с
http://packages.python.org/passlib/lib/passlib.hash.cta_pbkdf2_sha1.html#passlib.hash.cta_pbkdf2_sha1
private byte[] calculatePasswordDigest(char[] pass, byte[] salt, int iterations)
throws PasswordProtectionException
{
try
{
/* JCE Version (does not work as BC uses PKCS12 encoding)
SecretKeyFactory kf = SecretKeyFactory.getInstance("PBEWITHHMACSHA1","BC");
PBEKeySpec ks = new PBEKeySpec(pass, salt, iterations,160);
SecretKey digest = kf.generateSecret(ks);
return digest.getEncoded();
*/
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator();
gen.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(pass), salt, iterations);
byte[] derivedKey = ((KeyParameter)gen.generateDerivedParameters(160)).getKey();
return derivedKey;
}
catch(Exception e)
{
LOG.error("Failed to strengthen the password with PBKDF2.",e);
throw new PasswordProtectionException();
}
}
Ответ 3
PBKDF2WithHmacSHA1 уже поддерживается в BouncyCastle 1.60
https://www.bouncycastle.org/specifications.html Хеширование паролей и PBE
Тест пройден с помощью среды выполнения OpenJDK 18.9 (сборка 11.0.1 + 13):
Security.addProvider(new BouncyCastleProvider());
String password = "xrS7AJk+V6L8J?B%";
SecureRandom rnd = new SecureRandom();
int saltLength = 16;
int keyLength = 128;
int iterationCount = 10000;
byte[] salt = new byte[saltLength];
rnd.nextBytes(salt);
//SunJCE
SecretKeyFactory factorySun = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1", "SunJCE");
KeySpec keyspecSun = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength);
SecretKey keySun = factorySun.generateSecret(keyspecSun);
System.out.println(keySun.getClass().getName());
System.out.println(Hex.toHexString(keySun.getEncoded()));
//BouncyCastle
SecretKeyFactory factoryBC = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1", "BC");
KeySpec keyspecBC = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength);
SecretKey keyBC = factoryBC.generateSecret(keyspecBC);
System.out.println(keyBC.getClass().getName());
System.out.println(Hex.toHexString(keyBC.getEncoded()));
Assert.assertArrayEquals(keySun.getEncoded(), keyBC.getEncoded());
Выход:
com.sun.crypto.provider.PBKDF2KeyImpl
e9b01389fa91a6172ed6e95e1e1a2611
org.bouncycastle.jcajce.provider.symmetric.util.BCPBEKey
e9b01389fa91a6172ed6e95e1e1a2611