Java Cipher - PBE 线程安全问题
Java Cipher - PBE thread-safety issue
Cipher and/or 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 and/or PBEKeySpec 类 中是否存在错误?或者我在测试中遗漏了什么?
非常欢迎任何帮助。
更新
我倾向于认为这很可能是与终结和数组相关的 JVM 错误的表现。下面是一个更通用的测试用例。 运行 和 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 and the related issue https://bugs.openjdk.java.net/browse/JDK-8191002。
它已在 Java 2018 年 1 月 CPU 版本中得到修复和发布。
更新:JDK 9 及更高版本已通过使用 reachabilityFence() 修复此问题。
由于 JDK 的较早版本中缺少此围栏,您应该使用解决方法:« as first discovered by Hans Boehm, it just so happens that one way to implement the equivalent of reachabilityFence(x) even now is "synchronized(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);
}
Cipher and/or 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 and/or PBEKeySpec 类 中是否存在错误?或者我在测试中遗漏了什么?
非常欢迎任何帮助。
更新
我倾向于认为这很可能是与终结和数组相关的 JVM 错误的表现。下面是一个更通用的测试用例。 运行 和 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 and the related issue https://bugs.openjdk.java.net/browse/JDK-8191002。
它已在 Java 2018 年 1 月 CPU 版本中得到修复和发布。
更新:JDK 9 及更高版本已通过使用 reachabilityFence() 修复此问题。
由于 JDK 的较早版本中缺少此围栏,您应该使用解决方法:« as first discovered by Hans Boehm, it just so happens that one way to implement the equivalent of reachabilityFence(x) even now is "synchronized(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);
}