Search code examples
flutterdartencryptionaescryptojs

AES 128 CFB decryption Flutter/dart


I have an AES 128 CFB encrypted string and need to decrypt it.

Encrypted string: NpevHdSNWoXzXXzndH4WgRnM6QChqPwI8wguZ3iCOzGo4sG1RtHRiGGMkPcR2EaHOqJ22LQUcTM2BLKe6rQAdHJ58E/E/OpCY5wV45AYqJIgB2Yx4GbfeNeo0Do0AGhfVdbFFTybSElpJoLVdX5a4KWQVifKrw

Key: bKxh4vz1WpDnMlK7

I have javascript function which can decrypt successfully by using crypto-js library

const decryptURL = src => {
  var key = 'bKxh4vz1WpDnMlK7';
  var base64data = CryptoJS.enc.Base64.parse(src);
  var encrypted = new CryptoJS.lib.WordArray.init(
    base64data.words.slice(4),
    base64data.sigBytes - 16,
  );
  var iv = new CryptoJS.lib.WordArray.init(base64data.words.slice(0, 4));
  var cipher = CryptoJS.lib.CipherParams.create({ciphertext: encrypted});
  var decrypted = CryptoJS.AES.decrypt(cipher, CryptoJS.enc.Utf8.parse(key), {
    iv: iv,
    mode: CryptoJS.mode.CFB,
    padding: CryptoJS.pad.NoPadding,
  });
  return decrypted.toString(CryptoJS.enc.Utf8);
};

I have tried many solution but still no luck to decrypt it, below is my decrypt function

extension StringEx on String {
  Map<dynamic, dynamic> get decryptData {
    final key = encrypt.Key.fromUtf8('bKxh4vz1WpDnMlK7');
    final base64data = base64.decode(base64.normalize(this));
    final encrypted = base64data.sublist(4, base64data.length - 16); // Assuming 16 bytes are the IV in CryptoJS
    final iv = base64data.sublist(0, 4); // Assuming 16 bytes are the IV in CryptoJS

    final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cfb64, padding: null));
    final decryptedBytes = encrypter.decrypt(encrypt.Encrypted(encrypted), iv: encrypt.IV(iv));
    final dataMap = jsonDecode(decryptedBytes);

    return dataMap;
  }
}

Any help would be appreciated, thanks a lot!


Solution

  • The CryptoJS and Dart code are not compatible because 1. the segment sizes used are different and 2. the Dart code, unlike the CryptoJS code, can only process plaintexts whose length is an integer multiple of the segment size.

    • Regarding 1: With the CFB mode there is always a segment size associated. The segment size is the number of bits encrypted per encryption step and is often appended to the identifier, e.g. CFB-128 specifies CFB with a segment size of 128 bits.
      The CryptoJS code uses a segment size of 128 bits (CFB-128), while the Dart code uses a segment size of 64 bits (CFB-64). The segment size cannot be changed in either library, so on the Dart side the encrypt library cannot be used to decrypt the ciphertext of the CryptoJS code.
      Hence, on the Dart side, another library is needed that also supports CFB-128. One possibility is PointyCastle (note that the encrypt library is a wrapper over a sub-functionality of PointyCastle, so actually the same library is used, just directly).

    • Regarding 2: The CFB mode is a stream cipher mode and can therefore encrypt plaintexts of arbitrary length without padding.
      However, the CFB implementation of PointyCastle (and therefore of the encrypt library) can only encrypt plaintexts whose length is an integer multiple of the segment size, i.e. for CFB-128 an integer multiple of the block size. This generally requires additional padding, which is inefficient and shouldn't actually be necessary.
      Because of this constraint, conversely, to decrypt a ciphertext of the CryptoJS code with PointyCastle, the ciphertext must be padded to an integer multiple of the block size before decryption.
      For this, any padding can be used, e.g. zero padding (the latter is sufficient since the length information about the padding bytes is given by the original length of the ciphertext and therefore need not be contained in the padding).
      After decryption, the plaintext must be unpadded (i.e. shortened to the original length of the ciphertext).

    A possible implementation with PointyCastle that allows decryption of the CryptoJS ciphertext is:

    import 'dart:convert';
    import 'dart:typed_data';
    import 'package:pointycastle/export.dart';
    
    ...
    
    final dataB64 = "NpevHdSNWoXzXXzndH4WgRnM6QChqPwI8wguZ3iCOzGo4sG1RtHRiGGMkPcR2EaHOqJ22LQUcTM2BLKe6rQAdHJ58E/E/OpCY5wV45AYqJIgB2Yx4GbfeNeo0Do0AGhfVdbFFTybSElpJoLVdX5a4KWQVifKrw";
    print(dataB64.decryptData);
    
    ...
    
    extension StringEx on String {
      Map<dynamic, dynamic> get decryptData {
    
        // Get key
        final key = utf8.encode('bKxh4vz1WpDnMlK7');
    
        // Separate IV and ciphertext
        const aesBlockSize = 16;
        final base64data = base64.decode(base64.normalize(this));
        final encrypted = base64data.sublist(aesBlockSize);
        final iv = base64data.sublist(0, aesBlockSize);
    
        // Zero pad ciphertext
        final padLength = (aesBlockSize - (encrypted.length % aesBlockSize)) % aesBlockSize;
        final cipherBytesPadded = Uint8List(encrypted.length + padLength)..setAll(0, encrypted);
    
        // Decrypt
        final params = ParametersWithIV(KeyParameter(key), iv);
        final cipher = BlockCipher("AES/CFB-128")..init(false, params);
        final plainBytesPadded = Uint8List(cipherBytesPadded.length);
        for (var offset = 0; offset < cipherBytesPadded.length;) {
          final len = cipher.processBlock(cipherBytesPadded, offset, plainBytesPadded, offset);
          offset += len;
        }
    
        // Truncate plaintext to ciphertext size
        final plainBytes = plainBytesPadded.sublist(0, encrypted.length);
    
        return jsonDecode(utf8.decode(plainBytes)); // {error: Signature invalid, hash: 6c0ccf462bee20812224664ad0a8cc27, str: null10131696322798salt}
      }
    }