fluttergoencryption

How do you implement AES encryption in flutter?


I have a problem matching the encryption in flutter with the one in my BE service, the key and iv are the same in both systems, I dont know what I did wrong in the flutter, but it failed to decrypt the message sent by BE service. Here is the encrypt and decrypt function in the BE service.

func EncryptData(data string) (string, error) {
    var key = []byte(config.CipherConfig.Key)
    var iv = []byte(config.CipherConfig.Iv)

    block, err := aes.NewCipher(key)
    if err != nil {
        return "", err
    }

    ciphertext := make([]byte, aes.BlockSize+len(data))
    stream := cipher.NewCFBEncrypter(block, iv)
    stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(data))

    return hex.EncodeToString(ciphertext[aes.BlockSize:]), nil
}

// Function to decrypt data
func DecryptData(encryptedData string) (string, error) {
    var key = []byte(config.CipherConfig.Key)
    var iv = []byte(config.CipherConfig.Iv)

    block, err := aes.NewCipher(key)
    if err != nil {
        return "", err
    }

    ciphertext, err := hex.DecodeString(encryptedData)
    if err != nil {
        return "", err
    }

    stream := cipher.NewCFBDecrypter(block, iv)
    stream.XORKeyStream(ciphertext, ciphertext)

    return string(ciphertext), nil
}

And here the flutter part, I use encrypt package, and the result doesn't seem right. I am not the one who create the BE so I do not fully understand what it does, what do I need to change in my flutter code so the result matches? (the secret key and iv already same)

static String encrypt(String data) {
    final iv = IV.fromUtf8(Env.iv);
    final encrypter = Encrypter(AES(Key.fromUtf8(Env.secretKey), mode: AESMode.cfb64));
    return encrypter.encrypt(data, iv: iv).base16;
  }

  static String decrypt(String data) {
    final iv = IV.fromUtf8(Env.iv);
    final encrypter = Encrypter(AES(Key.fromUtf8(Env.secretKey), mode: AESMode.cfb64));
    return encrypter.decrypt16(data, iv: iv);
  }

Solution

  • The CFB mode requires the specification of a segment size (the segment size defines the number of bits encrypted per encryption step). The Go code uses 128 bits, the Dart code 64 bits, so that both codes are incompatible.

    The encrypt package does not allow changing the segment size, so this library must be replaced. encrypt is a wrapper over a few functionalities of the PointyCastle library. The latter supports a segment size of 128 bits, so that PointyCastle can be used directly.


    However, there is one drawback of the PointyCastle implementation that needs to be addressed: The CFB mode is a stream cipher mode that can encrypt plaintexts of any length without padding. The Go implementation takes this into account, but unfortunately the PointyCastle implementation does not and requires the plaintext length to be an integer multiple of the segment size.
    To encrypt plaintext of any length with the PointyCastle implementation, the plaintext must be padded to the required length before encryption and the resulting ciphertext must be shortened by the number of padding bytes. Since the plaintext length is known, any padding, e.g. zero padding, can be used.
    During decryption, the ciphertext must be padded to the required length before decryption and the resulting plaintext must be shortened by the number of padding bytes.


    The following Dart Code encrypts a plaintext of any length with CFB-128 using PointyCastle and is compatible with the Go Code:

    import 'dart:convert';
    import 'dart:typed_data';
    import 'package:convert/convert.dart';
    import 'package:pointycastle/export.dart';
    
    String encrypt(String plaintext){
    
      var key = utf8.encode("01234567890123456789012345678901"); // for the test only
      var iv = utf8.encode("abcdefghijklmnop");                  // for the test only
      var pt = utf8.encode(plaintext);
    
      var keyParam = KeyParameter(key);
      var params = ParametersWithIV(keyParam, iv);
    
      // Zero pad plaintext
      var aesBlockSize = 16;
      var padLength = (aesBlockSize - (pt.length % aesBlockSize)) % aesBlockSize;
      var ptPadded = Uint8List(pt.length + padLength)..setAll(0, pt);
    
      // Encrypt
      var cipher = BlockCipher("AES/CFB-128");
      cipher.init(true, params);
      var ctPadded = Uint8List(ptPadded.length);
      for (int offset = 0; offset < ptPadded.length;) {
        int len = cipher.processBlock(ptPadded, offset, ctPadded, offset);
        offset += len;
      }
    
      // Remove padding
      var ct = ctPadded.sublist(0, pt.length);
    
      return hex.encode(ct);
    }
    
    String decrypt(String ciphertext) {
    
      var key = utf8.encode("01234567890123456789012345678901"); // for the test only
      var iv = utf8.encode("abcdefghijklmnop");                  // for the test only
      var ct = hex.decode(ciphertext);
    
      var keyParam = KeyParameter(key);
      var params = ParametersWithIV(keyParam, iv);
    
      // Zero pad ciphertext
      var aesBlockSize = 16;
      var padLength = (aesBlockSize - (ct.length % aesBlockSize)) % aesBlockSize;
      var ctPadded = Uint8List(ct.length + padLength)..setAll(0, ct);
    
      // Decrypt
      var cipher = BlockCipher("AES/CFB-128");
      cipher.init(false, params);
      var ptPadded = Uint8List(ctPadded.length);
      for (int offset = 0; offset < ctPadded.length;) {
        int len = cipher.processBlock(ctPadded, offset, ptPadded, offset);
        offset += len;
      }
    
      // Remove padding
      var pt = ptPadded.sublist(0, ct.length);
    
      return utf8.decode(pt);
    }
    

    Note that in practice a random byte sequence should be used for the key. If the key material is a string, a key derivation function (such as PBKDF2) should be applied.
    Since the reuse of key/IV pairs is a vulnerability, a random IV should be used for each encryption if the key is fixed. The IV is not secret and is passed along with the ciphertext (usually concatenated).