Android 6 加密 Cipher.update 不再像 Android 5 那样工作
Android 6 crypto Cipher.update doesn't work anymore like Android 5
以下代码 在 Android 5 上 完美运行,
现在 Android 6 我有这个断言错误:
junit.framework.ComparisonFailure: expected:
This is clear te[xt right now]
but was:
This is clear te[]
at testAndroidAesCfbDecrypther(AesCfbCryptherTest.java:112)
此功能适用于 Motorola Moto G Android 5.1、Samsunsg S5 Android 5.1 和带有 Android 5.1 的模拟器。它不适用于 Motorola Moto G Android 6 和带有 Android 6.
的模拟器
public void testAndroidAesCfbDecrypther() {
Cipher AESCipher;
final String password = "th3ke1of16b1t3s0"; //password
final byte[] IV = Hex.toBytes("aabbccddeeff3a1224420b1d06174748"); //vector
final String expected = "This is clear text right now";
final byte[] encrypted1 = Hex.toBytes("a1ea8e1c4d8579b84e3e8d48d17fe916a70079b1bdc75841667cc15f");
final byte[] encrypted2 = Hex.toBytes("73052b25306059dda5d6880aa873383124448a38bcb3a769f6aed2f5");
try {
byte[] key = password.getBytes("US-ASCII");
key = Arrays.copyOf(key, 16); // use only first 128 bit
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec IVSpec = new IvParameterSpec(IV);
AESCipher = Cipher.getInstance("AES/CFB/NoPadding"); //Tried also with and without "BC" provider
AESCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, IVSpec);
byte[] dec1 = AESCipher.update(encrypted1);
String r = new String(dec1);
assertEquals(expected, r); //assert fail here
byte[] dec2 = AESCipher.update(encrypted2);
r = new String(dec2);
assertEquals(expected, r);
} catch (NoSuchAlgorithmException e) {
...
}
}
出于测试目的,我也尝试使用 'doFinal',但第二个断言失败:
ByteArrayOutputStream bytesStream1 = new ByteArrayOutputStream();
byte[] dec1 = AESCipher.update(encrypted1);
bytesStream1.write(dec1);
byte[] dec2 = AESCipher.doFinal();
bytesStream1.write(dec2);
r = new String(bytesStream1.toByteArray());
assertEquals(expected, r); //ASSERTION OKAY
ByteArrayOutputStream bytesStream2 = new ByteArrayOutputStream();
dec1 = AESCipher.update(encrypted2);
bytesStream2.write(dec1);
dec2 = AESCipher.doFinal();
bytesStream2.write(dec2);
r = new String(bytesStream2.toByteArray());
assertEquals(expected, r); //ASSERTION FAIL
作为测试,我在 ruby 中尝试了同样的事情并且它起作用了:
require 'openssl'
expected = "This is clear text right now"
encrypted1 = ["a1ea8e1c4d8579b84e3e8d48d17fe916a70079b1bdc75841667cc15f"].pack('H*')
encrypted2 = ["73052b25306059dda5d6880aa873383124448a38bcb3a769f6aed2f5"].pack('H*')
decipher = OpenSSL::Cipher.new('AES-128-CFB')
decipher.decrypt
decipher.key = "th3ke1of16b1t3s0" #password
decipher.iv = ["aabbccddeeff3a1224420b1d06174748"].pack('H*') #vector
puts "TEST1-------------------"
puts (decipher.update(encrypted1) + decipher.final) == expected ? "OK" : "FAIL"
puts "------------------------"
puts "TEST2-------------------"
puts (decipher.update(encrypted2) + decipher.final) == expected ? "OK" : "FAIL"
puts "------------------------"
分组密码有许多不同的操作模式。有些像 CBC 需要额外的填充,因为只能加密块大小的倍数,但其他像 CFB 是没有填充的流模式。
如果您使用填充,那么合同是完整的块是从 Cipher#update
编辑的 return,但是必须填充或取消填充的最后一个块只能 由 Cipher#doFinal
.
编辑而成 return
因为CFB模式不需要padding,所以确实不应该有这个限制,但是那样的话你就改契约了,因为现在Cipher#update
可以return不完整的数据。如果即使对于 CFB 模式也要强制执行此合同,那么实施将是一致的,甚至可能更容易(因为 CFB 的中间值和移位寄存器)。
你真的需要自己完成解密并合并输出。使用 ByteArrayOutputStream
很容易做到这一点,但您也可以使用三个 System.arraycopy
调用。
ByteArrayOutputStream fullPlaintextStream = new ByteArrayOutputStream();
byte[] dec1 = AESCipher.update(encrypted1);
fullPlaintextStream.write(dec1);
byte[] dec2 = AESCipher.update(encrypted2);
fullPlaintextStream.write(dec2);
byte[] dec3 = AESCipher.doFinal();
fullPlaintextStream.write(dec3);
r = new String(fullPlaintextStream.toByteArray());
assertEquals(expected, r);
Android 5.1 和 6.0 之间存在差异,因为提供者发生了变化
Android 有多个用于不同算法的 JCE 提供程序。在这种特定情况下,BouncyCastle 提供程序 ("BC") 和 AndroidOpenSSL 提供程序之间存在重叠,因为它们都同时支持 AES-CFB,但 AndroidOpenSSL 更高在提供者列表中,因此它具有优先权。自己看看这个:
for(Provider p : Security.getProviders()) {
System.out.println("Provider " + p.getName());
for(Map.Entry e : p.entrySet()) {
System.out.println(" " + e.getKey() + " : " + e.getValue());
}
}
最后,Android 6.0 删除了 CFB(corresponding commit). Compare the providers for 5.1.1 and 6.0.1。因此在 Android 6 中只有 BouncyCastle 提供程序支持 CFB 模式,其工作方式与此答案的第一部分。
可能的解决方案:
将 Android 6 中的提供程序替换为旧版本的 conscrypt(来自 Android 5 的那个)。
CFB 是一种流模式,所以这个事实可以用来围绕 Cipher
class 编写包装器,使 CFB 始终 return 相同的数量传入的输出字节数。想法是用 0x00 字节填充不完整的块,并将相应的输出字节与下一个 update
调用的第一个字节进行 XOR 以产生一些输出。
尝试 运行 java - jdk 1.6 中的代码,但失败了。
以下是我尝试过的方法,如果它有任何帮助 - (修改为默认情况下能够 运行 在 eclipse 中):
public static void testAndroidAesCfbDecrypther() {
Cipher AESCipher;
final String password = "th3ke1of16b1t3s0"; //password
final byte[] IV = DatatypeConverter.parseHexBinary("aabbccddeeff3a1224420b1d06174748"); //vector
final String expected = "This is clear text right now";
final byte[] encrypted1 = DatatypeConverter.parseHexBinary("a1ea8e1c4d8579b84e3e8d48d17fe916a70079b1bdc75841667cc15f");
final byte[] encrypted2 = DatatypeConverter.parseHexBinary("73052b25306059dda5d6880aa873383124448a38bcb3a769f6aed2f5");
try {
byte[] key = password.getBytes("US-ASCII");
key = Arrays.copyOf(key, 16); // use only first 128 bit
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec IVSpec = new IvParameterSpec(IV);
AESCipher = Cipher.getInstance("AES/CFB/NoPadding"); //Tried also with and without "BC" provider
AESCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, IVSpec);
byte[] dec1 = AESCipher.update(encrypted1);
String r = new String(dec1);
assertEquals(expected, r); //assert fail here
byte[] dec2 = AESCipher.update(encrypted2);
r = new String(dec2);
assertEquals(expected, r);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
}
private static void assertEquals(String left, String right) {
System.out.println(left+":"+right);
System.out.println(left.equals(right));
}
输出:
This is clear text right now:This is clear te
false
This is clear text right now:xt right nowThis is clear text r
false
可能是默认缓冲区大小已更改。
可以运行上面两个模拟器中的和post一样吗?
下面的代码也有助于识别所使用的 CipherSpi 实现(假设安全经理没有抱怨):
private static void printCipherDetails(Cipher cipher) {
try {
for(Field field : cipher.getClass().getDeclaredFields() ){
field.setAccessible(true);
if( field.getType() == javax.crypto.CipherSpi.class ) {
Object object = field.get(cipher);
System.out.print("Name :"+field.getName()+". ");
if( object != null ) {
System.out.println("CipherSpi :"+object.getClass());
}
else {
System.out.println("CipherSpi not initialized!");
}
}
else if( field.getType() == java.security.Provider.class ) {
Object object = field.get(cipher);
System.out.print("Name :"+field.getName()+". ");
if( object != null ) {
System.out.println("Provider :"+object.getClass());
}
else {
System.out.println("Provider not initialized!");
}
}
}
}catch (Exception e) {
e.printStackTrace();
}
System.out.println("");
}
在 cipher.init()
之后调用时,打印如下所示的详细信息:
Name :b. Provider :class com.sun.crypto.provider.SunJCE
Name :c. CipherSpi :class com.sun.crypto.provider.AESCipher
Name :j. CipherSpi not initialized!
以下代码 在 Android 5 上 完美运行, 现在 Android 6 我有这个断言错误:
junit.framework.ComparisonFailure: expected:
This is clear te[xt right now]
but was:
This is clear te[]
at testAndroidAesCfbDecrypther(AesCfbCryptherTest.java:112)
此功能适用于 Motorola Moto G Android 5.1、Samsunsg S5 Android 5.1 和带有 Android 5.1 的模拟器。它不适用于 Motorola Moto G Android 6 和带有 Android 6.
的模拟器public void testAndroidAesCfbDecrypther() {
Cipher AESCipher;
final String password = "th3ke1of16b1t3s0"; //password
final byte[] IV = Hex.toBytes("aabbccddeeff3a1224420b1d06174748"); //vector
final String expected = "This is clear text right now";
final byte[] encrypted1 = Hex.toBytes("a1ea8e1c4d8579b84e3e8d48d17fe916a70079b1bdc75841667cc15f");
final byte[] encrypted2 = Hex.toBytes("73052b25306059dda5d6880aa873383124448a38bcb3a769f6aed2f5");
try {
byte[] key = password.getBytes("US-ASCII");
key = Arrays.copyOf(key, 16); // use only first 128 bit
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec IVSpec = new IvParameterSpec(IV);
AESCipher = Cipher.getInstance("AES/CFB/NoPadding"); //Tried also with and without "BC" provider
AESCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, IVSpec);
byte[] dec1 = AESCipher.update(encrypted1);
String r = new String(dec1);
assertEquals(expected, r); //assert fail here
byte[] dec2 = AESCipher.update(encrypted2);
r = new String(dec2);
assertEquals(expected, r);
} catch (NoSuchAlgorithmException e) {
...
}
}
出于测试目的,我也尝试使用 'doFinal',但第二个断言失败:
ByteArrayOutputStream bytesStream1 = new ByteArrayOutputStream();
byte[] dec1 = AESCipher.update(encrypted1);
bytesStream1.write(dec1);
byte[] dec2 = AESCipher.doFinal();
bytesStream1.write(dec2);
r = new String(bytesStream1.toByteArray());
assertEquals(expected, r); //ASSERTION OKAY
ByteArrayOutputStream bytesStream2 = new ByteArrayOutputStream();
dec1 = AESCipher.update(encrypted2);
bytesStream2.write(dec1);
dec2 = AESCipher.doFinal();
bytesStream2.write(dec2);
r = new String(bytesStream2.toByteArray());
assertEquals(expected, r); //ASSERTION FAIL
作为测试,我在 ruby 中尝试了同样的事情并且它起作用了:
require 'openssl'
expected = "This is clear text right now"
encrypted1 = ["a1ea8e1c4d8579b84e3e8d48d17fe916a70079b1bdc75841667cc15f"].pack('H*')
encrypted2 = ["73052b25306059dda5d6880aa873383124448a38bcb3a769f6aed2f5"].pack('H*')
decipher = OpenSSL::Cipher.new('AES-128-CFB')
decipher.decrypt
decipher.key = "th3ke1of16b1t3s0" #password
decipher.iv = ["aabbccddeeff3a1224420b1d06174748"].pack('H*') #vector
puts "TEST1-------------------"
puts (decipher.update(encrypted1) + decipher.final) == expected ? "OK" : "FAIL"
puts "------------------------"
puts "TEST2-------------------"
puts (decipher.update(encrypted2) + decipher.final) == expected ? "OK" : "FAIL"
puts "------------------------"
分组密码有许多不同的操作模式。有些像 CBC 需要额外的填充,因为只能加密块大小的倍数,但其他像 CFB 是没有填充的流模式。
如果您使用填充,那么合同是完整的块是从 Cipher#update
编辑的 return,但是必须填充或取消填充的最后一个块只能 由 Cipher#doFinal
.
因为CFB模式不需要padding,所以确实不应该有这个限制,但是那样的话你就改契约了,因为现在Cipher#update
可以return不完整的数据。如果即使对于 CFB 模式也要强制执行此合同,那么实施将是一致的,甚至可能更容易(因为 CFB 的中间值和移位寄存器)。
你真的需要自己完成解密并合并输出。使用 ByteArrayOutputStream
很容易做到这一点,但您也可以使用三个 System.arraycopy
调用。
ByteArrayOutputStream fullPlaintextStream = new ByteArrayOutputStream();
byte[] dec1 = AESCipher.update(encrypted1);
fullPlaintextStream.write(dec1);
byte[] dec2 = AESCipher.update(encrypted2);
fullPlaintextStream.write(dec2);
byte[] dec3 = AESCipher.doFinal();
fullPlaintextStream.write(dec3);
r = new String(fullPlaintextStream.toByteArray());
assertEquals(expected, r);
Android 5.1 和 6.0 之间存在差异,因为提供者发生了变化
Android 有多个用于不同算法的 JCE 提供程序。在这种特定情况下,BouncyCastle 提供程序 ("BC") 和 AndroidOpenSSL 提供程序之间存在重叠,因为它们都同时支持 AES-CFB,但 AndroidOpenSSL 更高在提供者列表中,因此它具有优先权。自己看看这个:
for(Provider p : Security.getProviders()) {
System.out.println("Provider " + p.getName());
for(Map.Entry e : p.entrySet()) {
System.out.println(" " + e.getKey() + " : " + e.getValue());
}
}
最后,Android 6.0 删除了 CFB(corresponding commit). Compare the providers for 5.1.1 and 6.0.1。因此在 Android 6 中只有 BouncyCastle 提供程序支持 CFB 模式,其工作方式与此答案的第一部分。
可能的解决方案:
将 Android 6 中的提供程序替换为旧版本的 conscrypt(来自 Android 5 的那个)。
CFB 是一种流模式,所以这个事实可以用来围绕
Cipher
class 编写包装器,使 CFB 始终 return 相同的数量传入的输出字节数。想法是用 0x00 字节填充不完整的块,并将相应的输出字节与下一个update
调用的第一个字节进行 XOR 以产生一些输出。
尝试 运行 java - jdk 1.6 中的代码,但失败了。 以下是我尝试过的方法,如果它有任何帮助 - (修改为默认情况下能够 运行 在 eclipse 中):
public static void testAndroidAesCfbDecrypther() {
Cipher AESCipher;
final String password = "th3ke1of16b1t3s0"; //password
final byte[] IV = DatatypeConverter.parseHexBinary("aabbccddeeff3a1224420b1d06174748"); //vector
final String expected = "This is clear text right now";
final byte[] encrypted1 = DatatypeConverter.parseHexBinary("a1ea8e1c4d8579b84e3e8d48d17fe916a70079b1bdc75841667cc15f");
final byte[] encrypted2 = DatatypeConverter.parseHexBinary("73052b25306059dda5d6880aa873383124448a38bcb3a769f6aed2f5");
try {
byte[] key = password.getBytes("US-ASCII");
key = Arrays.copyOf(key, 16); // use only first 128 bit
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec IVSpec = new IvParameterSpec(IV);
AESCipher = Cipher.getInstance("AES/CFB/NoPadding"); //Tried also with and without "BC" provider
AESCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, IVSpec);
byte[] dec1 = AESCipher.update(encrypted1);
String r = new String(dec1);
assertEquals(expected, r); //assert fail here
byte[] dec2 = AESCipher.update(encrypted2);
r = new String(dec2);
assertEquals(expected, r);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
}
private static void assertEquals(String left, String right) {
System.out.println(left+":"+right);
System.out.println(left.equals(right));
}
输出:
This is clear text right now:This is clear te
false
This is clear text right now:xt right nowThis is clear text r
false
可能是默认缓冲区大小已更改。
可以运行上面两个模拟器中的和post一样吗?
下面的代码也有助于识别所使用的 CipherSpi 实现(假设安全经理没有抱怨):
private static void printCipherDetails(Cipher cipher) {
try {
for(Field field : cipher.getClass().getDeclaredFields() ){
field.setAccessible(true);
if( field.getType() == javax.crypto.CipherSpi.class ) {
Object object = field.get(cipher);
System.out.print("Name :"+field.getName()+". ");
if( object != null ) {
System.out.println("CipherSpi :"+object.getClass());
}
else {
System.out.println("CipherSpi not initialized!");
}
}
else if( field.getType() == java.security.Provider.class ) {
Object object = field.get(cipher);
System.out.print("Name :"+field.getName()+". ");
if( object != null ) {
System.out.println("Provider :"+object.getClass());
}
else {
System.out.println("Provider not initialized!");
}
}
}
}catch (Exception e) {
e.printStackTrace();
}
System.out.println("");
}
在 cipher.init()
之后调用时,打印如下所示的详细信息:
Name :b. Provider :class com.sun.crypto.provider.SunJCE
Name :c. CipherSpi :class com.sun.crypto.provider.AESCipher
Name :j. CipherSpi not initialized!