Skip to content
Advertisement

Flutter / Dart AES-256-CBC decrypting from encryption in PHP

Could anyone help me to figure out a decryption algorithm for data that is encrypted in PHP using AES-256-CBC. I have tried lots of different ways, but I think I am messing up when trying to replicate the method of recreating they Key/IV in Dart and keep getting exceptions such as:

RangeError (end): Invalid value: Not in inclusive range 0..16:

The PHP code that does the encryption (which cannot be changed as the encrypted strings are provided by a third party) is as follows:

function encrypt( $string, $encrypt=true) {
    $secret_key = 'SuperSecretKey';
    $secret_iv = 'SuperSecretBLOCK';
    $output = false;
    $encrypt_method = "AES-256-CBC";
    $key = hash( 'sha256', $secret_key );
    $iv = substr( hash( 'sha256', $secret_iv ), 0, 16 );
    if($encrypt) {
        $output = base64_encode( openssl_encrypt( $string, $encrypt_method, $key, 0, $iv ) );
    } else {
        $output = openssl_decrypt( base64_decode( $string ), $encrypt_method, $key, 0, $iv );
    }
    return $output;
}

For example, if the encryption routine in PHP were called to encrypt the string “This is a Test!”, the result would be:

ZHArWURDY2FkelBtSGY5c1AzdTNBZz09

It is this result that I am attempting to decrypt in Dart and not having any luck!

Here is what I have so far that is resulting in the exception referenced above:

import 'package:encrypt/encrypt.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert' show utf8;

String extractPayload(String payload) {
      String strPwd = 'SuperSecretKey';
      String strIv = 'SuperSecretBLOCK';
      var iv = sha256.convert(utf8.encode(strIv));
      var key = sha256.convert(utf8.encode(strPwd));
      IV ivObj = IV.fromUtf8(iv.toString());
      Key keyObj = Key.fromUtf8(key.toString());
      final encrypter = Encrypter(AES(keyObj));
      final decrypted = encrypter.decrypt(Encrypted.from64(payload), iv: ivObj);
      print(decrypted);
      return decrypted;
}

Any suggestions welcome, Thanks

Advertisement

Answer

The PHP code has a number of unnecessary weaknesses that complicate the porting:

  • When hashing, the result is returned as a hexadecimal encoded string, which unfortunately is the default of the hash function. It would make more sense to return the result as binary data (but for this, the third parameter would have to be explicitly set to TRUE). The return as hexadecimal string has two disadvantages in this context:
  1. Since each byte is represented by two characters, the 32 bytes hash of SHA256 is represented by 64 characters, which is 64 bytes. This is used as the AES-256 key (because binary data was probably wrongly assumed during the implementation), resulting in an invalid key, since an AES-256 key is exactly 32 bytes in size. PHP solves this problem by truncating keys that are too long (too short keys are padded with 0 values), i.e. only the first 32 bytes are used for the key. With regard to cross-platform implementations, it would be more useful to display an error message in case of an invalid key instead of secretly generating a valid key applying arbitrary and in-transparent rules.
  2. For hexadecimal strings, upper or lower case letters can be used for the digits (a-f) depending on the platform. If this is the case for a cross-platform implementation, different keys are generated. Here this is not critical, because PHP and Dart use lower case letters.
  • openssl_encrypt Base64 encodes the ciphertext by default, openssl_decrypt expects a Base64 encoded ciphertext by default. This can be disabled with the flag OPENSSL_RAW_DATA, but this is not the case in the current code. At the same time the ciphertext is explicitly Base64 encoded during encryption, so that it is double Base64 encoded. Similarly when decrypting the ciphertext it is explicitly Base64 decoded, so that it is also double Base64 decoded. This redundancy is pointless, only inflates the ciphertext unnecessarily and reduces performance.

In addition, the Dart code assumes wrong default values: The encrypt package uses SIC (or CTR) as default mode. Since CBC is specified in the PHP code, this mode must be explicitly specified in the Dart code.

The following Dart implementation decrypts the ciphertext ZHArWURDY2FkelBtSGY5c1AzdTNBZz09:

import 'package:encrypt/encrypt.dart' as EncryptPack;
import 'package:crypto/crypto.dart' as CryptoPack;
import 'dart:convert' as ConvertPack;

String extractPayload(String payload) {
    String strPwd = "SuperSecretKey";
    String strIv = 'SuperSecretBLOCK';
    var iv = CryptoPack.sha256.convert(ConvertPack.utf8.encode(strIv)).toString().substring(0, 16);         // Consider the first 16 bytes of all 64 bytes
    var key = CryptoPack.sha256.convert(ConvertPack.utf8.encode(strPwd)).toString().substring(0, 32);       // Consider the first 32 bytes of all 64 bytes
    EncryptPack.IV ivObj = EncryptPack.IV.fromUtf8(iv);
    EncryptPack.Key keyObj = EncryptPack.Key.fromUtf8(key);
    final encrypter = EncryptPack.Encrypter(EncryptPack.AES(keyObj, mode: EncryptPack.AESMode.cbc));        // Apply CBC mode
    String firstBase64Decoding = new String.fromCharCodes(ConvertPack.base64.decode(payload));              // First Base64 decoding
    final decrypted = encrypter.decrypt(EncryptPack.Encrypted.fromBase64(firstBase64Decoding), iv: ivObj);  // Second Base64 decoding (during decryption)
    return decrypted;
}

The following test returns the plaintext This is a Test!

String plaintext = extractPayload("ZHArWURDY2FkelBtSGY5c1AzdTNBZz09");
print(plaintext); // This is a Test!
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement