【发布时间】:2018-04-08 20:48:47
【问题描述】:
我的 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 类中是否存在错误?还是我在测试中遗漏了什么?
非常欢迎任何帮助。
更新
【问题讨论】:
-
在 Windows 7 SP1 上与 jdk1.8.0_112 的结果相同。作为 JUnit 测试运行时很少可重现。在发布模式下作为应用运行时更常见。
-
用 jdk1.8.0_151 和 9.0.1+11 测试过,还是有问题
-
有趣,如果它会产生使用线程执行器而不是并行流运行的任何错误。
-
嗯,直接替换为 10 线程
ThreadPollExecutor
和Runnable
到目前为止,在 1000 万次迭代中没有显示任何问题。尝试使用 JDK 1.8.0_112 和 1.7.0_55。 -
另一种理论 -
com.sun.crypto.provider.PBKDF2KeyImpl
具有终结器,其中key:byte[]
被重置为零并设置为空。可能,虽然看似不可能,但在执行解码时会以某种方式影响密钥。如果我在PBKDF2KeyImpl.getEncoded()
中停止调试器并在方法返回之前用0 填充this.key
,似乎我得到完全相同的错误。即使终结器与此无关,但至少有一个事实可能是正确的——编码值是用全零密钥解密的。
标签: java encryption thread-safety aes pbkdf2