Java Cipher – PBE线程安全问题

我似乎有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"); } 

输出:

 Try #656684 | Unexpected decoded value: [ jE |S   ] Try #33896 | Unexpected decoded value: [ jE |S   ] 2 of 1000000 decodes failed 

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

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


UPDATE

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

我倾向于认为这很可能是与终结和数组相关的JVM错误的表现。 下面是一个更通用的测试用例。 使用java -Xmx10m -cp . UnexpectedArrayContents运行java -Xmx10m -cp . UnexpectedArrayContents java -Xmx10m -cp . UnexpectedArrayContents ,堆越小越容易失败。 不确定对clone()调用是否真的重要,只是试图接近原始代码段。

 // Omitting package and imports for brevity // ... public class UnexpectedArrayContents { void demonstrate() { IntStream.range(0, 20000000).parallel().forEach(i -> { String expected = randomAlphaNumeric(10); byte[] expectedBytes = expected.getBytes(StandardCharsets.UTF_8); ArrayHolder holder = new ArrayHolder(expectedBytes); byte[] actualBytes = holder.getBytes(); String actual = new String(actualBytes, StandardCharsets.UTF_8); if (!Objects.equals(expected, actual)) { System.err.println("attempt#" + i + " failed; expected='" + expected + "' actual='" + actual + "'"); System.err.println("actual bytes: " + DatatypeConverter.printHexBinary(actualBytes)); } }); } static class ArrayHolder { private byte[] _bytes; ArrayHolder(final byte[] bytes) { _bytes = bytes.clone(); } byte[] getBytes() { return _bytes.clone(); } @Override protected void finalize() throws Throwable { if (_bytes != null) { Arrays.fill(_bytes, (byte) 'z'); _bytes = null; } super.finalize(); } } private static final String ALPHA_NUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final Random RND = new Random(); static String randomAlphaNumeric(int count) { final StringBuilder sb = new StringBuilder(); while (count-- != 0) { int character = RND.nextInt(ALPHA_NUMERIC_STRING.length()); sb.append(ALPHA_NUMERIC_STRING.charAt(character)); } return sb.toString(); } public static void main(String[] args) throws Exception { new UnexpectedArrayContents().demonstrate(); } } 

更新

现在该错误被跟踪为JDK-8191002 。 受影响的版本:8,9,10。

它确实是PBKDF2KeyImpl.getEncoded()方法中的JDK错误。

错误报告中的更多细节https://bugs.openjdk.java.net/browse/JDK-8191177以及相关问题https://bugs.openjdk.java.net/browse/JDK-8191002 。

它已在Java January 2018 CPU版本中修复和发布。

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

由于在早期版本的JDK中缺少这个围栏,你应该使用一种解决方法:« 正如Hans Boehm首先发现的那样,实现相当于reachabilityFence(x)的一种方法就是“同步”(x) ){}“ »

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

 SecretKeyFactory factory = SecretKeyFactory.getInstance(pbkdf2Algorithm); KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt.getBytes(charset), iterationCount, keyStrength); SecretKey secret = factory.generateSecret(spec); SecretKeySpec key; //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized(secret) { key = new SecretKeySpec(secret.getEncoded(), keyAlgorithm); }