1

I need to decrypt an AES (PKCS#7) encoded string in my Flutter mobile application.

The string is got from a QR Code, which has been generated from a Java application and contains the AES encoded String.

The Java encoding :

import java.security.Security;
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class MyClass {

     public static void main(String[] args) throws Exception {
         String toEncode = "firstname.lastname@mycompany.com;12";
         String encoded = pleaseEncodeMe(toEncode);
         System.out.println(encoded);
     }

     private static String pleaseEncodeMe(String plainText) throws Exception {
         Security.addProvider(new BouncyCastleProvider());
         final String encryptionAlgorithm = "AES/CBC/PKCS7PADDING";
         final String encryptionKey = "WHatAnAWEsoMeKey";
         final SecretKeySpec keySpecification = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), encryptionAlgorithm);
         final Cipher cipher = Cipher.getInstance(encryptionAlgorithm, "BC");
         cipher.init(Cipher.ENCRYPT_MODE, keySpecification);
         final byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
         return Base64.encodeBase64URLSafeString(encryptedBytes);
    }

}

Output : AIRTEuNmSuQtYuysv93w3w83kJJ6sg7kaU7XzA8xrAjOp-lKYPp1brtDAPbhSJmT

The Dart decoding :

void main() {
    print(decodeMeOrDie("AIRTEuNmSuQtYuysv93w3w83kJJ6sg7kaU7XzA8xrAjOp-lKYPp1brtDAPbhSJmT"));
}

String decodeMeOrDie(String encryptedString) {
    final key = Key.fromUtf8("WHatAnAWEsoMeKey");
    final iv = IV.fromLength(16);
    final encrypter = Encrypter(AES(key, mode: AESMode.cbc, padding: "PKCS7"));
    return encrypter.decrypt64(encryptedString, iv: iv);
}

Output : Y��=X�Rȑ�"Qme@mycompany.com;12

You can see that only a part of the string is decoded.

Yann39
  • 14,285
  • 11
  • 56
  • 84
  • The IV that was used for the encryption is required for the decryption. The IV doesn't need to be kept secret and is usually prepended to the ciphertext. The data are then Base64 encoded together. On the decryption side, after Base64 decoding, both parts are separated again. By the way: It is also common´ to generate the IV explicitly, e.g. [here](https://stackoverflow.com/a/29267873/9014097). – Topaco Sep 11 '19 at 16:43
  • But the IV should be [pseudo]random generated right ? The Cipher is supposed to generate it so I trusted Java on this point. If I explicitly use `SecureRandom` as stated in the link you provided, still how would I get it on Dart side for decryption ? – Yann39 Sep 12 '19 at 06:37
  • OK I just saw that I can pass it as 3rd parameter of the cipher initialization, so I added `new IvParameterSpec("ThisIVisAmazing!".getBytes(StandardCharsets.UTF_8))` to the `cipher.init` method, then on Dart side I now use `final iv = encrypt.IV.fromUtf8("ThisIVisAmazing!");` and finally get the right decrypted string :) Maybe just wondering if using hardcoded key and IV is a good practice ? BTW you can post your comment as an answer I would be happy to accept it. – Yann39 Sep 12 '19 at 07:04
  • Be careful: It would be the wrong approach to hardcode the IV on both sides. The IV is generated on the encryption-side, passed to the decryption-side together with the ciphertext, and used there for decryption, i.e. the IV is not fixed, but rather dynamic, see my answer. – Topaco Sep 12 '19 at 17:34
  • can you please share final code of dart? – Bhavin Patel Aug 14 '20 at 06:48
  • Sure, I posted it as an answer as it is too long for a comment. Best. – Yann39 Aug 14 '20 at 07:56

2 Answers2

2
  • Two things must be taken into account:

    1) For decryption, the IV used for encryption is required.

    2) For security reasons, a new IV must be randomly generated for each encryption so that no IV is used more than once with the same key, here.

    Therfore, the IV must be passed from the encryption-side to the decryption-side. This doesn't happen automatically, but has to be implemented.

  • One possibility is to concatenate the byte-arrays of IV and ciphertext. Usually the IV is placed before the ciphertext and the result is Base64-encoded (if required), e.g. in Java:

    // Concatenate IV and ciphertext
    byte[] iv = ...
    byte[] ciphertext = ...
    byte[] ivAndCiphertext = new byte[iv.length + ciphertext.length];
    System.arraycopy(iv, 0, ivAndCiphertext, 0, iv.length);
    System.arraycopy(ciphertext, 0, ivAndCiphertext, iv.length, ciphertext.length);
    // If required: Base64-encoding
    

    This data is transmitted to the decryption-side, which separates both parts after Base64-decoding. In the case of AES-CBC, the IV is 16 bytes long, so the first 16 bytes represent the IV and the rest the ciphertext. The IV doesn't need to be encrypted because it isn't secret.

    Specifically for your case this means that you have to concatenate IV and ciphertext on the Java-side and to Base64-encode the result. On the Dart-side you have to Base64-decode first and then both parts, IV and ciphertext, can be separated and used for the following decryption.

  • There are two ways to generate the IV before encryption: Implicit generation by the Cipher-instance as in your example or explicit generation e.g. via SecureRandom. Both alternatives are discussed here. If the IV is generated implicitly (via the Cipher-instance), then this IV must be determined via the Cipher-instance, since it is later required for decryption:

    // Determine IV from cipher for later decryption
    byte[] iv = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
    

    If the IV is determined explicitly (e.g. using SecureRandom), it must be passed to the Cipher-instance so that it will be used in the running encryption. This is done using an IvParameterSpec.

    // Assign IV to cipher so that it is used for current encryption
    byte[] iv = ...
    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
    cipher.init(Cipher.ENCRYPT_MODE, secretkeySpec, ivParameterSpec);
    
  • A hard-coded key is in general not good practice (except for testing purposes perhaps). However, the topic of key generation/management is outside the scope of this answer. There are already a lot of questions and answers on this subject. If your question is not covered by these answers, please post a new question. A hard-coded IV doesn't occur within the above architecture and should only be used for testing purposes.


Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Much clearer now, thanks @Topaco. Got it to work by prepending the IV to the cipher text. I now have to generate the key and share it to the client, I found some nice doc on the net so I should not get any problem. Best! – Yann39 Sep 13 '19 at 06:55
0

If it can help someone, here is the code I ended up with, in dart (it uses the encrypt package) :

/// Decode the specified QR code encrypted string
static String decodeQrCode(String encryptedString) {
  try {
    // pad the encrypted base64 string with '=' characters until length matches a multiple of 4
    final int toPad = encryptedString.length % 4;
    if (toPad != 0) {
      encryptedString = encryptedString.padRight(encryptedString.length + toPad, "=");
    }

    // get first 16 bytes which is the initialization vector
    final iv = encrypt.IV(Uint8List.fromList(base64Decode(encryptedString).getRange(0, 16).toList()));

    // get cipher bytes (without initialization vector)
    final encrypt.Encrypted encrypted = encrypt.Encrypted(Uint8List.fromList(
        base64Decode(encryptedString).getRange(16, base64Decode(encryptedString).length).toList()));

    // decrypt the string using the key and the initialization vector
    final key = encrypt.Key.fromUtf8(YOUR_KEY);
    final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: "PKCS7"));
    return encrypter.decrypt(encrypted, iv: iv);
  } catch (e) {
    _log.severe("Error while decoding QR code : $e");
    return null;
  }
}
Yann39
  • 14,285
  • 11
  • 56
  • 84
  • I'm using `Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")` in java . How to use in dart? – Bhavin Patel Aug 14 '20 at 08:17
  • "AES/CBC/PKCS5Padding" is actually equivalent to PKCS7 in Java, this is just a naming legacy... PKCS5 padding cannot be used for AES encryption. So you should not get any problem to decrypt it in Dart using PKCS7. The Dart encrypt package does not support PKCS5 anyway. – Yann39 Aug 14 '20 at 09:08
  • yeah but it's throw **Invalid argument(s): Invalid or corrupted pad block** – Bhavin Patel Aug 14 '20 at 09:28
  • You prepended the initialization vector to the encrypted text in Java right ? I cannot help more with Java encryption, and this is out of the purpose of this thread. If you still have problem I think you better go with creating your own question. – Yann39 Aug 14 '20 at 11:03
  • @bdevloper were you able to get any solution? – Yakub Pasha Jan 05 '21 at 03:32
  • @YakubPasha No I didn't . That was my side learning and I'm not working on that anymore. maybe in future – Bhavin Patel Jan 05 '21 at 04:27