Java 中带有 bouncycastle 的 PBKDF2

Posted

技术标签:

【中文标题】Java 中带有 bouncycastle 的 PBKDF2【英文标题】:PBKDF2 with bouncycastle in Java 【发布时间】:2012-01-30 05:38:20 【问题描述】:

我正在尝试将密码安全地存储在数据库中,为此我选择存储使用 PBKDF2 函数生成的哈希值。我想使用充气城堡库来做到这一点,但我不知道为什么我不能通过使用 JCE 接口让它工作...... 问题在于以 3 种不同的模式生成哈希: 1.使用sun提供的PBKDF2WithHmacSHA1密钥工厂 2.直接使用充气城堡api 3.通过JCE使用充气城堡 产生 2 个不同的值:一个与前两个通用,一个与第三个通用。

这是我的代码:

    //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”作为算法,我从 bouncy castle java 文档中获取。

输出如下:

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

有什么想法吗?

【问题讨论】:

我不确定“模式 3”在做什么,但我会忽略它。它的输出是 160 位,而不是您要求的 128 位。 160 位是 SHA-1 哈希的大小。为了便携性,我会坚持使用“模式 1”。 我同意 erickson 的观点 - 您是否真的需要使用“模式 3”,或者“模式 1”是否可以用于安全存储密码?顺便说一下,这里是您的第一个问题的详细问题。 确实没必要。我只是想了解为什么 BouncyCastle 的 PBEWITHHMACSHA1 不做同样的事情。我同意,由于可移植性问题,我不会选择第二种方法。 谢谢伙计,第 1 号在工作中救了我的命:D 不同之处在于 BTW 如何将密码转换为字节。 pKcs5 和 pkcs12 不同(utf16 与 UTF8 相比,它有一堆可预测的 0 字节。这就是为什么我更喜欢模式 3 并希望看到一个 jce 参数来请求它。但同样,SHA1 无论如何都死了(不是在这个特定的申请,但您只是不想与审核员讨论) 【参考方案1】:

简而言之,产生差异的原因是模式 #1 和 #2 中的 PBKDF2 算法使用 PKCS #5 v2 方案 2 (PKCS5S2) 进行迭代密钥生成,但模式 #3 中“PBEWITHHMACSHA1”的 BouncyCastle 提供程序使用PKCS #12 v1 (PKCS12) 算法。这些是完全不同的密钥生成算法,因此您会得到不同的结果。

下面解释了为什么会这样以及为什么会得到不同大小的结果的更多详细信息。

首先,当您构建 JCE KeySpec 时,keyLength 参数仅向提供者表达您想要的密钥大小的“偏好”。来自the API docs:

注意:这用于指示对可变密钥大小密码的密钥长度的偏好。实际的密钥大小取决于每个提供者的实现。

从the source of JCEPBEKey 判断,Bouncy Castle 提供商似乎不尊重此参数,因此您应该期望在使用 JCE API 时从任何使用 SHA-1 的 BC 提供商那里获得一个 160 位密钥。

您可以通过以编程方式访问测试代码中返回的keybc 变量上的getKeySize() 方法来确认这一点:

Key keybc = factorybc.generateSecret(keyspecbc);
// ...
Method getKeySize = JCEPBEKey.class.getDeclaredMethod("getKeySize");
getKeySize.setAccessible(true);
System.out.println(getKeySize.invoke(keybc)); // prints '160'

现在,要了解“PBEWITHHMACSHA1”提供者对应什么,您可以在the source for 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);
    

您可以在上面看到,此密钥工厂使用 PKCS #12 v1 (PKCS12) 算法进行迭代密钥生成。但是您要用于密码散列的 PBKDF2 算法使用 PKCS #5 v2 方案 2 (PKCS5S2)。这就是为什么你会得到不同的结果。

我快速浏览了在 BouncyCastleProvider 中注册的 JCE 提供程序,但根本看不到 任何 使用 PKCS5S2 的密钥生成算法,更不用说也将它与 HMAC 一起使用的算法了- SHA-1。

所以我猜你要么坚持使用 Sun 实现(上面的模式 #1)而失去在其他 JVM 上的可移植性,要么直接使用 Bouncy Castle 类(上面的模式 #2)并在运行时需要 BC 库。

无论哪种方式,您都应该切换到 160 位密钥,这样就不会不必要地截断生成的 SHA-1 哈希。

【讨论】:

【参考方案2】:

我找到了一种 BC Crypto-Only 方法(实际上来自 BC 的 cms 包),它可以生成基于 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】:

BouncyCastle 1.60 已经支持 PBKDF2WithHmacSHA1

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

【讨论】:

以上是关于Java 中带有 bouncycastle 的 PBKDF2的主要内容,如果未能解决你的问题,请参考以下文章

BouncyCastle 密钥转换 - Java pkcs1格式,pkcs8格式互转

安装BouncyCastle

无法使用java中的pkcs#7和bouncyCastle签署zip文件

向证书请求添加属性,java + bouncycastle 1.48

使用 Bouncycastle 在 Java 中进行格式保留加密 (FPE)

Java-国密算法SM2实现(bouncycastle)