Java Cipher - PBE线程安全问题

Lay*_*ain 5 java encryption aes thread-safety pbkdf2

我似乎有Cipher和/或PBEKeySpec的线程安全问题.

  • JDK:1.8.0_102,1.8.0_151和9.0.1 + 11
  • PBKDF2算法:PBKDF2WithHmacSHA1
  • 密码算法:AES/CFB/NoPadding
  • 关键算法:AES

我知道如果我们使用相同的实例,这些类不是安全的,但事实并非如此,我在每次解码时都会得到一个新的实例.但即便如此,有时解码失败,也没有例外,只是意外的解码值.

我已经能够重现这个问题:

@Test
public void shouldBeThreadSafe() {

    final byte[] encoded = {
        27, 26, 18, 88, 84, -87, -40, -91, 70, -74, 87, -21, -124,
        -114, -44, -24, 7, -7, 104, -26, 45, 96, 119, 45, -74, 51
    };
    final String expected = "dummy data";
    final Charset charset = StandardCharsets.UTF_8;

    final String salt = "e47312da-bc71-4bde-8183-5e25db6f0987";
    final String passphrase = "dummy-passphrase";

    // Crypto configuration
    final int iterationCount = 10;
    final int keyStrength = 128;
    final String pbkdf2Algorithm = "PBKDF2WithHmacSHA1";
    final String cipherAlgorithm = "AES/CFB/NoPadding";
    final String keyAlgorithm = "AES";

    // Counters
    final AtomicInteger succeedCount = new AtomicInteger(0);
    final AtomicInteger failedCount = new AtomicInteger(0);

    // Test
    System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "10");
    IntStream.range(0, 1000000).parallel().forEach(i -> {
        try {

            SecretKeyFactory factory = SecretKeyFactory.getInstance(pbkdf2Algorithm);
            KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt.getBytes(charset), iterationCount, keyStrength);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKeySpec key = new SecretKeySpec(tmp.getEncoded(), keyAlgorithm);
            Cipher cipher = Cipher.getInstance(cipherAlgorithm);


            int blockSize = cipher.getBlockSize();
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(encoded, blockSize));
            byte[] dataToDecrypt = Arrays.copyOfRange(encoded, blockSize, encoded.length);
            cipher.init(Cipher.DECRYPT_MODE, key, iv);
            byte[] utf8 = cipher.doFinal(dataToDecrypt);

            String decoded = new String(utf8, charset);
            if (!expected.equals(decoded)) {
                System.out.println("Try #" + i + " | Unexpected decoded value: [" + decoded + "]");
                failedCount.incrementAndGet();
            } else {
                succeedCount.incrementAndGet();
            }
        } catch (Exception e) {
            System.out.println("Try #" + i + " | Decode failed");
            e.printStackTrace();
            failedCount.incrementAndGet();
        }
    });

    System.out.println(failedCount.get() + " of " + (succeedCount.get() + failedCount.get()) + " decodes failed");
}
Run Code Online (Sandbox Code Playgroud)

输出:

Try #656684 | Unexpected decoded value: [?jE    |S???]
Try  #33896 | Unexpected decoded value: [?jE    |S???]

2 of 1000000 decodes failed
Run Code Online (Sandbox Code Playgroud)

我不明白这段代码怎么会失败,Cipher和/或PBEKeySpec类中是否有错误?或者我在测试中遗漏了什么?

任何帮助都会非常受欢迎.


UPDATE

OpenJDK问题:https://bugs.openjdk.java.net/browse/JDK-8191177

Lay*_*ain 4

这确实是该方法中的 JDK bug PBKDF2KeyImpl.getEncoded()

\n\n

更多详细信息请参见错误报告https://bugs.openjdk.java.net/browse/JDK-8191177和相关问题https://bugs.openjdk.java.net/browse/JDK-8191002

\n\n

它已在 Java 2018 年 1 月 CPU 版本中修复并发布。

\n\n

更新:JDK 9 及更高版本已通过使用reachabilityFence() 修复了此问题。

\n\n

由于 JDK 的早期版本中缺少此栅栏,您应该使用一种解决方法: \xc2\xab ,这是 Hans Boehm 首次发现的,碰巧的是,即使是现在,实现等效的reachabilityFence(x)的一种方法是“同步(x) {}" \xc2\xbb

\n\n

在我们的例子中,解决方法是:

\n\n
SecretKeyFactory factory = SecretKeyFactory.getInstance(pbkdf2Algorithm);\nKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt.getBytes(charset), iterationCount, keyStrength);\nSecretKey secret = factory.generateSecret(spec);\nSecretKeySpec key;\n//noinspection SynchronizationOnLocalVariableOrMethodParameter\nsynchronized(secret) {\n  key = new SecretKeySpec(secret.getEncoded(), keyAlgorithm);\n}\n
Run Code Online (Sandbox Code Playgroud)\n