3

The following code worked perfectly on Android 5, now on Android 6 I have this assert error:

junit.framework.ComparisonFailure: expected:

This is clear te[xt right now]

but was:

This is clear te[]

at testAndroidAesCfbDecrypther(AesCfbCryptherTest.java:112)

This function works on Motorola Moto G Android 5.1, Samsunsg S5 Android 5.1 and emulator with Android 5.1. It doesn't work on Motorola Moto G Android 6 and emulator with 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) {
        ...
  }

}

For testing purposes i tried also with 'doFinal', but second assertion fails:

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

Just as a test I tried the same thing in ruby and it works:

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 "------------------------"
Community
  • 1
  • 1
Sebtm
  • 7,002
  • 8
  • 29
  • 32
  • Re-posting the comment below - (where I originally intended it to be posted) - Can you post the [default-charset](http://developer.android.com/reference/java/nio/charset/Charset.html#defaultCharset%28%29) as well for both the scenarios ? – Ravindra HV Mar 13 '16 at 17:53
  • @RavindraHV default charset is utf-8 for both case, but I think in this case it does not matter, because special characters are not used. – Sebtm Mar 14 '16 at 07:00
  • 1
    There is a problem with your `doFinal()` variant -- you must not call `doFinal()` twice as it resets the `Cipher` object (read the [doc](https://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html#doFinal%28byte[],%20int%29)). – vlp Mar 14 '16 at 21:25

2 Answers2

2

Block ciphers have many different modes of operation. Some like CBC require an additional padding, because only multiples of the block size can be encrypted, but others like CFB are streaming modes without padding.

If you use padding, then the contract is that full blocks are returned from Cipher#update, but the last block that must be padded or unpadded, can only be returned from Cipher#doFinal.

Since CFB mode doesn't need padding, it really shouldn't have this restriction, but then you would have changed the contract, because now Cipher#update can return incomplete data. If this contract is to be enforced even for CFB mode, then the implementation will be consistent and possibly even easier (because of intermediate values and the shift register of CFB).

You really need to finish the decryption and combine the output yourself. It's easy to do this with a ByteArrayOutputStream, but you can also use three System.arraycopy calls.

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);

Discrepancy between Android 5.1 and 6.0 because of changes in providers

Android has multiple JCE providers for different algorithms. In this specific case, there was overlap between the BouncyCastle provider ("BC") and AndroidOpenSSL provider, because both of them supported AES-CFB at the same time, but AndroidOpenSSL was higher up in the provider list, so it took precendence. See for yourself with this:

for(Provider p : Security.getProviders()) {
    System.out.println("Provider " + p.getName());
    for(Map.Entry e : p.entrySet()) {
        System.out.println("    " + e.getKey() + " : " + e.getValue());
    }
}

Finally, CFB was removed for Android 6.0 (corresponding commit). Compare the providers for 5.1.1 and 6.0.1. So in Android 6 only the BouncyCastle provider supports CFB mode, which works in the same way as described in the first part of this answer.


Possible solutions:

  • Replace the provider in Android 6 with an older version of conscrypt (the one from Android 5).

  • CFB is a streaming mode, so this fact can be used to write a wrapper around the Cipher class for CFB to always return the same amount of output bytes as were passed in. The idea is to fill up incomplete blocks with 0x00 bytes and XOR the corresponding output bytes with the first bytes of the next update call to produce some output.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • Why there was this change now (for the better), your guess is as good as mine, but you could look into the bouncycastle mailing list if there is something about that. – Artjom B. Mar 03 '16 at 20:13
  • I can not call doFinal because I do not know in advance when I will stop receiving data. However I need to decrypt the bytes as they become available. – Sebtm Mar 04 '16 at 07:01
  • 1
    Then I guess you have to wait for a block to be completed. There is no (sane) way to change that behavior. – Artjom B. Mar 04 '16 at 10:02
  • Can you post the [default-charset](http://developer.android.com/reference/java/nio/charset/Charset.html#defaultCharset%28%29) for both the scenarios ? – Ravindra HV Mar 12 '16 at 15:31
  • @RavindraHV I don't see how this has anything to do with a charset. This is basically a change in the internal state of the CFBBlockCipher (and there was a change from BC1.50 included in Android 5 and BC1.52 included in Android 6, but that change would have resulted in a different observation). – Artjom B. Mar 12 '16 at 16:38
  • @Sebtm I've researched a bit. I cannot reproduce your Android 5 result. I've tried it with a lot of different BouncyCastle libraries (1.48-1.54) and they all behave in the same way as you describe for Android 6. Maybe there is something wrong with your Android 5 version/phone. Can you clarify the exact version and hardware for both? – Artjom B. Mar 12 '16 at 19:50
  • The comment was intended for @Sebtm. Any how, a change in the default-charset would cause a change in how the string was reconstructed from bytes is it not? The OP is using US-ASCII while creating but not so while re-constructing... – Ravindra HV Mar 13 '16 at 17:51
  • @RavindraHV I've thought about that, but I don't think it's possible, because the recovered plaintext is correct up to a point and the same ciphertext is used for both cases. The only thing I could think of would be a \0 terminating the string, but this is Java and not C. – Artjom B. Mar 13 '16 at 18:28
  • @RavindraHV default charset is utf-8 for both case, but I think in this case it does not matter, because special characters are not used. – Sebtm Mar 14 '16 at 06:57
  • @Artjom B. function testAndroidAesCfbDecrypther() works on Motorola Moto G Android 5.1, Samsunsg S5 Android 5.1 and emulator with Android 5.1. It doesn't work on Motorola Moto G Android 6 and emulator with Android 6. – Sebtm Mar 14 '16 at 06:57
  • Using [CipherInputStream](https://docs.oracle.com/javase/7/docs/api/javax/crypto/CipherInputStream.html) may be even easier than `ByteArrayInputStream` in some cases. – vlp Mar 14 '16 at 21:55
  • 1
    @vlp You might be right, but CipherInputStream and CipherOutputStream are known to swallow some errors, so this makes some things more complicated. – Artjom B. Mar 14 '16 at 22:10
  • @ArtjomB. Didn't know that (and found out it is pretty nasty for old jre [1](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/javax/crypto/CipherInputStream.java#109) [2](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/javax/crypto/CipherInputStream.java#299), later one seems [better](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/javax/crypto/CipherInputStream.java?av=f)). Thank you. – vlp Mar 14 '16 at 22:23
  • @Sebtm Now I've had some time to extend my answer. – Artjom B. Mar 18 '16 at 11:49
  • @ArtjomB I try with one of these two solutions, I think there is no alternative. – Sebtm Mar 19 '16 at 13:37
0

Tried to run the code in java - jdk 1.6 but it fails. Below is what I tried, if its of any help - (modified to be able to run in eclipse by default) :

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));
}

Output :

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

May be the default buffer sizes changed.

Can you run the above in the two emulators and post the same ?


Also below code helps in identifying the CipherSpi implementation used (assuming security manager does not complain) :

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("");
}

When invoked after cipher.init(), prints details such as noted below :

Name :b. Provider :class com.sun.crypto.provider.SunJCE
Name :c. CipherSpi :class com.sun.crypto.provider.AESCipher
Name :j. CipherSpi not initialized!
Ravindra HV
  • 2,558
  • 1
  • 17
  • 26
  • output of printCipherDetails on Android 5.1 emulator (WORK) `Name :provider. Provider :class com.android.org.conscrypt.OpenSSLProvider Name :specifiedProvider. Provider not initialized! Name :specifiedSpi. CipherSpi not initialized! Name :spiImpl. CipherSpi :class com.android.org.conscrypt.OpenSSLCipher$AES$CFB` – Sebtm Mar 15 '16 at 16:01
  • output of printCipherDetails on Android 6.0 Motorola Moto G (FAIL) `Name :provider. Provider :class com.android.org.bouncycastle.jce.provider.BouncyCastleProvider Name :specifiedProvider. Provider not initialized! Name :specifiedSpi. CipherSpi not initialized! Name :spiImpl. CipherSpi :class com.android.org.bouncycastle.jcajce.provider.symmetric.AES$ECB` – Sebtm Mar 15 '16 at 16:05
  • output of printCipherDetails on Android 6.0 emulator (FAIL) `Name :provider. Provider :class com.android.org.bouncycastle.jce.provider.BouncyCastleProvider Name :specifiedProvider. Provider not initialized! Name :specifiedSpi. CipherSpi not initialized! Name :spiImpl. CipherSpi :class com.android.org.bouncycastle.jcajce.provider.symmetric.AES$ECB` – Sebtm Mar 15 '16 at 16:07
  • Can you post the output for the modified program (`public static void testAndroidAesCfbDecrypther()`) of your initial code as well ? It would be helpful to see the values that are printed. Also it appears that Android 5.0 is using `Name :spiImpl. CipherSpi :class com.android.org.conscrypt.OpenSSLCipher$AES$CFB` where as 6.0 is using `Name :spiImpl. CipherSpi :class com.android.org.bouncycastle.jcajce.provider.symmetric.AES$ECB` and 6.0 does not even appear to be using 'CFB' which is what you would have specified but is using 'ECB' instead which is clearly not right! – Ravindra HV Mar 15 '16 at 19:08
  • 1
    See if you can include the [source code](https://android.googlesource.com/platform/external/conscrypt/+/lollipop-wear-release/src/main/java/org/conscrypt/OpenSSLCipher.java) for Android 5.0 using the constructor and see if it works in 6.0. – Ravindra HV Mar 15 '16 at 19:14
  • 1
    Also see if [this](http://stackoverflow.com/questions/34286798/javax-crypto-cipher-working-differently-since-android-6-marshmallow) link helps. – Ravindra HV Mar 15 '16 at 19:14
  • The output of printCipherDetails comes already from inside testAndroidAesCfbDecrypther() called just after 'AESCipher.init'. Source code is always the same. I will try to check why the wrong Provider is initalized. It looks as if Android 6 does not support anymore CFB format. – Sebtm Mar 15 '16 at 21:03