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!