3

I have encrypted a file in c# code using RijndaelManaged which is available in System.Security.Cryptography. This file needs to be transferred to a mobile app developed using dart/flutter and I need it to be decrypted using dart code and present it to the user. How can this be done?

Below shown is the code to do the encryption in c#:

            string password = keyPhrase; // Your Key Here
            UnicodeEncoding UE = new UnicodeEncoding();
            byte[] key = UE.GetBytes(password);

            string cryptFile = outputFile;
            FileStream fsCrypt = new FileStream(cryptFile, FileMode.Create);

            RijndaelManaged RMCrypto = new RijndaelManaged();

            CryptoStream cs = new CryptoStream(fsCrypt,
                RMCrypto.CreateEncryptor(key, key),
                CryptoStreamMode.Write);

            FileStream fsIn = new FileStream(inputFile, FileMode.Open);

            int data;
            while ((data = fsIn.ReadByte()) != -1)
                cs.WriteByte((byte)data);


            fsIn.Close();
            cs.Close();
            fsCrypt.Close();

Thank you

Richard Heap
  • 48,344
  • 9
  • 130
  • 112
Sajad Jaward
  • 247
  • 5
  • 16
  • Did you try writing any code in Flutter? – Chetan Nov 03 '19 at 06:51
  • @ChetanRanpariya no I couldn't try anything on flutter yet as I couldn't find any solution – Sajad Jaward Nov 03 '19 at 08:32
  • @ChetanRanpariya i have a flutter package https://pub.dev/packages/encrypt but i do not understand how to use it with the file instead of simple texts – Sajad Jaward Nov 03 '19 at 08:34
  • You should try reading your file like this: `Encrypted(File(filePath).readAsBytesSync())` And then pass this instance to decrypt function. – Igor Kharakhordin Nov 03 '19 at 10:29
  • Check out the comprehensive Dart package: https://pub.dev/packages/pointycastle – Richard Heap Nov 03 '19 at 14:09
  • @RichardHeap Thank you for the suggestion. Can you also please suggest me which decryption algorithm is suitable from the above encryption code? – Sajad Jaward Nov 03 '19 at 14:26
  • @IgorKharakhordin I tried your method, and my code looks like this `final decrypted = encrypter.decryptBytes(encryptedFile, );` but i am receiving an error which is `NoSuchMethodError (NoSuchMethodError: The getter 'bytes' was called on null.` – Sajad Jaward Nov 03 '19 at 14:28
  • @IgorKharakhordin I suspected that error would occur because the IV is not passed, so I passed it in this way: `final decrypted = encrypter.decryptBytes(encryptedFile, iv: encrypt.IV.fromBase64(encryptedFile.base64));` but then again I am getting another exception thrown which is: `RangeError (RangeError (end): Invalid value: Not in range 0..16, inclusive: 90448)`. Any idea why this is happening? – Sajad Jaward Nov 03 '19 at 14:32
  • Change AES mode to AESMode.cbc (which is C# default if I got it right). Create Encrypted instance by passing byte array to it (readAsBytesSync method returns you this array). – Igor Kharakhordin Nov 03 '19 at 14:44
  • @IgorKharakhordin I changed the mode to cbc and now i am getting a different exception which is: `ArgumentError (Invalid argument(s): Initialization vector must be the same length as block size)`. I will share my whole code in the next comment so that you can have a better view of it. – Sajad Jaward Nov 03 '19 at 14:52
  • @IgorKharakhordin `final fileBytes = File(savedDir + '/' + filename); final key = 'xxxxxxxx' //same 8 char key used in c# to encrypt the file final encryptedFile = Encrypted(fileBytes.readAsBytesSync()); final encrypter = Encrypter(AES(Key.fromUtf8(key), mode: AESMode.cbc)); final decrypted = encrypter.decryptBytes(encryptedFile, iv: IV.fromLength(fileBytes.lengthSync())); final decryptedByts = Uint8List.fromList(decrypted); final s = Image.memory(decryptedByts);` – Sajad Jaward Nov 03 '19 at 14:57
  • @IgorKharakhordin the exception is thrown right after the `decryptBytes` function execution – Sajad Jaward Nov 03 '19 at 14:58
  • Default IV size in C# is 16. Try to set it. – Igor Kharakhordin Nov 03 '19 at 15:07
  • @IgorKharakhordin i tried setting it to 16 and it threw an error like this: `ArgumentError (Invalid argument(s): Key length must be 128/192/256 bits)`, then i tried setting 256 but it thew me this exception: `ArgumentError (Invalid argument(s): Initialization vector must be the same length as block size)`. should I take my `key` and convert into bytes and get the length? – Sajad Jaward Nov 03 '19 at 15:19
  • 16 bytes = 128 bits – Igor Kharakhordin Nov 03 '19 at 15:20
  • @IgorKharakhordin changed it to `128` and now this exception: `ArgumentError (Invalid argument(s): Initialization vector must be the same length as block size)`. Im not even sure if I should use `IV.fromLength`, i got options like `.fromBase16`, `.fromBase64`, `.fromSecuerRandom` and `.fromUtf8` – Sajad Jaward Nov 03 '19 at 15:27
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/201791/discussion-between-sajad-jaward-and-igor-kharakhordin). – Sajad Jaward Nov 03 '19 at 15:28

2 Answers2

2

I ran into the same problem. After many hours, a solution was found. My code is based on this question1 and question2 Code on C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            var m_strPassPhrase = "YYYYYYYYYYYYYYYYYYY";
            var p_strSaltValue = "XXXXXXXXXXXXXXXXX";
            var m_strPasswordIterations = 2;
            var m_strInitVector = "ZZZZZZZZZZZZZZZZ";
            var plainText = "myPassword";
            var blockSize = 32;
        var saltValueBytes = Encoding.ASCII.GetBytes(p_strSaltValue);
        var password = new Rfc2898DeriveBytes(m_strPassPhrase, saltValueBytes, m_strPasswordIterations);
        var keyBytes = password.GetBytes(blockSize);

        var symmetricKey = new RijndaelManaged();

        var initVectorBytes = Encoding.ASCII.GetBytes(m_strInitVector);
        var encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes);

        var memoryStream = new System.IO.MemoryStream();
        var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);

        var plainTextBytes = Encoding.UTF8.GetBytes(plainText);

        cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
        cryptoStream.FlushFinalBlock();

        var cipherTextBytes = memoryStream.ToArray();
        memoryStream.Close();
        cryptoStream.Close();

        var cipherText = Convert.ToBase64String(cipherTextBytes);

        Console.WriteLine(cipherText);

        Console.WriteLine("\n end");
    }
}
}

For flutter you can use pointycastle Code on Dart(use decryptString and cryptString methods):

import 'dart:convert';
import 'package:pointycastle/block/aes_fast.dart';
import 'dart:typed_data';
import 'package:pointycastle/export.dart';
import 'package:pointycastle/key_derivators/pbkdf2.dart';
import 'package:pointycastle/paddings/pkcs7.dart';
import 'package:pointycastle/pointycastle.dart';

const KEY_SIZE = 32; // 32 byte key for AES-256
const ITERATION_COUNT = 2;
const SALT = "XXXXXXXXXXXXXXXXX";
const INITIAL_VECTOR = "ZZZZZZZZZZZZZZZZ";
const PASS_PHRASE = "YYYYYYYYYYYYYYYYYYY";

Future<String> cryptString(String text) async {
  String encryptedString = "";

  final mStrPassPhrase = toUtf8(PASS_PHRASE);

  encryptedString =
      AesHelper.encrypt(mStrPassPhrase, toUtf8(text), mode: AesHelper.CBC_MODE);

  return encryptedString;
}

Future<String> decryptString(String text) async {
  String decryptedString = "";

  final mStrPassPhrase = toUtf8(PASS_PHRASE);

  decryptedString =
      AesHelper.decrypt(mStrPassPhrase, toUtf8(text), mode: AesHelper.CBC_MODE);

  return decryptedString;
}

///MARK: AesHelper class
class AesHelper {
  static const CBC_MODE = 'CBC';
  static const CFB_MODE = 'CFB';

  static Uint8List deriveKey(dynamic password,
      {String salt = '',
      int iterationCount = ITERATION_COUNT,
      int derivedKeyLength = KEY_SIZE}) {
    if (password == null || password.isEmpty) {
      throw new ArgumentError('password must not be empty');
    }

    if (password is String) {
      password = createUint8ListFromString(password);
    }

    Uint8List saltBytes = createUint8ListFromString(salt);
    Pbkdf2Parameters params =
        new Pbkdf2Parameters(saltBytes, iterationCount, derivedKeyLength);
    KeyDerivator keyDerivator =
        new PBKDF2KeyDerivator(new HMac(new SHA1Digest(), 64));
    keyDerivator.init(params);

    return keyDerivator.process(password);
  }

  static Uint8List pad(Uint8List src, int blockSize) {
    var pad = new PKCS7Padding();
    pad.init(null);

    int padLength = blockSize - (src.length % blockSize);
    var out = new Uint8List(src.length + padLength)..setAll(0, src);
    pad.addPadding(out, src.length);

    return out;
  }

  static Uint8List unpad(Uint8List src) {
    var pad = new PKCS7Padding();
    pad.init(null);

    int padLength = pad.padCount(src);
    int len = src.length - padLength;

    return new Uint8List(len)..setRange(0, len, src);
  }

  static String encrypt(String password, String plaintext,
      {String mode = CBC_MODE}) {
    String salt = toASCII(SALT);
    Uint8List derivedKey = deriveKey(password, salt: salt);
    KeyParameter keyParam = new KeyParameter(derivedKey);
    BlockCipher aes = new AESFastEngine();

    var ivStr = toASCII(INITIAL_VECTOR);
    Uint8List iv =
        createUint8ListFromString(ivStr);

    BlockCipher cipher;
    ParametersWithIV params = new ParametersWithIV(keyParam, iv);
    switch (mode) {
      case CBC_MODE:
        cipher = new CBCBlockCipher(aes);
        break;
      case CFB_MODE:
        cipher = new CFBBlockCipher(aes, aes.blockSize);
        break;
      default:
        throw new ArgumentError('incorrect value of the "mode" parameter');
        break;
    }
    cipher.init(true, params);

    Uint8List textBytes = createUint8ListFromString(plaintext);
    Uint8List paddedText = pad(textBytes, aes.blockSize);
    Uint8List cipherBytes = _processBlocks(cipher, paddedText);

    return base64.encode(cipherBytes);
  }

  static String decrypt(String password, String ciphertext,
      {String mode = CBC_MODE}) {
    String salt = toASCII(SALT);
    Uint8List derivedKey = deriveKey(password, salt: salt);
    KeyParameter keyParam = new KeyParameter(derivedKey);
    BlockCipher aes = new AESFastEngine();

    var ivStr = toASCII(INITIAL_VECTOR);
    Uint8List iv = createUint8ListFromString(ivStr);
    Uint8List cipherBytesFromEncode = base64.decode(ciphertext);

    Uint8List cipherIvBytes =
        new Uint8List(cipherBytesFromEncode.length + iv.length)
          ..setAll(0, iv)
          ..setAll(iv.length, cipherBytesFromEncode);

    BlockCipher cipher;

    ParametersWithIV params = new ParametersWithIV(keyParam, iv);
    switch (mode) {
      case CBC_MODE:
        cipher = new CBCBlockCipher(aes);
        break;
      case CFB_MODE:
        cipher = new CFBBlockCipher(aes, aes.blockSize);
        break;
      default:
        throw new ArgumentError('incorrect value of the "mode" parameter');
        break;
    }
    cipher.init(false, params);

    int cipherLen = cipherIvBytes.length - aes.blockSize;
    Uint8List cipherBytes = new Uint8List(cipherLen)
      ..setRange(0, cipherLen, cipherIvBytes, aes.blockSize);
    Uint8List paddedText = _processBlocks(cipher, cipherBytes);
    Uint8List textBytes = unpad(paddedText);

    return new String.fromCharCodes(textBytes);
  }

  static Uint8List _processBlocks(BlockCipher cipher, Uint8List inp) {
    var out = new Uint8List(inp.lengthInBytes);

    for (var offset = 0; offset < inp.lengthInBytes;) {
      var len = cipher.processBlock(inp, offset, out, offset);
      offset += len;
    }

    return out;
  }
}

///MARK: HELPERS
Uint8List createUint8ListFromString(String s) {
  Uint8List ret = Uint8List.fromList(s.codeUnits);

  return ret;
}

String toUtf8(value) {
  var encoded = utf8.encode(value);
  var decoded = utf8.decode(encoded);
  return decoded;
}

String toASCII(value) {
  var encoded = ascii.encode(value);
  var decoded = ascii.decode(encoded);
  return decoded;
}
isHidden
  • 860
  • 5
  • 16
  • textBytes seems to be coming has a base64 string for me. When i use any only base64 decoder I do see the text I want but within flutter app I am not able to decode it the same way. Also, when I use online decoders as well, I get my secret string but its along with some other random characters – Aswin Francis Feb 11 '22 at 15:10
  • I've implemented C# and Flutter sides successfully, but special characters didn't work like "Ğ, Ş". How to fix this problem? – blackkara Mar 20 '22 at 00:32
1

The default mode of Rijndael in .Net is 128 bit block size - compatible with AES. Unless you are using a non-standard block size, prefer .Net's AesManaged.

You haven't specified which padding or mode you are using. The .Net default seems to be CBC, so we'll assume that. It's not clear whether it defaults to a certain padding mode.

(Note that you are using the key both as the IV and the key. The IV should be unique for each invocation of the encryption routine. TLDR - the way you are using AesManaged is insecure - don't use this code in real life.)

Also, you are decoding the key from a string. The key length of AES must be exactly 128 or 256 bits (or one of the more unusual ones). Unless you have chosen your string well, it is unlikely to UTF-8 encode to an exact key length. Also, by using a string you are only using bytes in the key that happen to be characters. Typically, to use a string as a password you would convert it to a key using a key derivation algorithm (e.g. PBKDF2) rather than just UTF-8 encoding it.

With all that said, if your password is exactly 16 (or 32 long) and your file is an exact multiple of 16 bytes (if it is not, you need to decide how to pad it) you should be able to decrypt it like this:

import 'dart:convert';
import 'dart:io';

import 'package:pointycastle/export.dart';

main() async {
  var key = utf8.encode('abcdefghijklmnop');

  var cipher = CBCBlockCipher(AESFastEngine())
    ..init(false, ParametersWithIV<KeyParameter>(KeyParameter(key), key));

  var cipherText = await File('encryptedFile').readAsBytes();
  var plainText = cipher.process(cipherText);

  await File('decryptedFile').writeAsBytes(plainText, flush: true);
}
Richard Heap
  • 48,344
  • 9
  • 130
  • 112
  • Thank you for replying! I face a conflict when providing `keys`. In your example, it is "abcdefghijklmnop" but in my .NET code, it accepts a password with length of 8. When I provide the same password as dart in my .NET side, it throws me this error: `Specified initialization vector (IV) does not match the block size for this algorithm`, but when I encrypt the file with an 8 char password, it works. Can you also help me on figuring out why? Thank you – Sajad Jaward Nov 05 '19 at 17:12
  • Did you switch to `AesManaged`? You should. – Richard Heap Nov 05 '19 at 17:18
  • Aah, .net is using utf16 encoding. So an 8 character password converts to 16 bytes. Either change that to utf8, or dart to utf16. The real solution is really to use pbkdf2 or similar. – Richard Heap Nov 06 '19 at 11:35