Decrypt AES/CBC/PKCS5Padding Encryption in Dart

3,784

The code can be simplified by using existing Dart libraries for the conversion binary to hex and vice versa. PointyCastle also supports the (PKCS7) padding, so that a custom implementation is not necessary, which also reduces the code. On the Internet you can find several dart implementations for AES/CBC/PKCS7Padding in combination with PBKDF2 that use PointyCastle, e.g. here and here.

A possible Dart implementation for decryption using the pointycastle and convert package is e.g. (for simplicity without exception handling):

import 'dart:typed_data';
import "package:pointycastle/export.dart";
import 'package:convert/convert.dart';
import 'dart:convert';
...
static Uint8List decrypt(Uint8List ciphertext, Uint8List key, Uint8List iv) {
  CBCBlockCipher cipher = new CBCBlockCipher(new AESFastEngine());
  ParametersWithIV<KeyParameter> params = new ParametersWithIV<KeyParameter>(new KeyParameter(key), iv);
  PaddedBlockCipherParameters<ParametersWithIV<KeyParameter>, Null> paddingParams = new PaddedBlockCipherParameters<ParametersWithIV<KeyParameter>, Null>(params, null);
  PaddedBlockCipherImpl paddingCipher = new PaddedBlockCipherImpl(new PKCS7Padding(), cipher);
  paddingCipher.init(false, paddingParams);
  return paddingCipher.process(ciphertext);
}

static Uint8List generateKey(Uint8List salt, Uint8List passphrase){
  KeyDerivator derivator = new PBKDF2KeyDerivator(new HMac(new SHA1Digest(), 64));
  Pbkdf2Parameters params = new Pbkdf2Parameters(salt, 5, 16);
  derivator.init(params);
  return derivator.process(passphrase);
}

With the posted test data:

String saltHex = '00000000000000000000000000000000';
String ivHex = '00000000000000000000000000000000';
String passphraseUtf8 = 'Mayur12354673645';
String ciphertextBase64 = "XjxCg0KK0ZDWa4XMFhykIw==";

Uint8List salt = hex.decode(saltHex);
Uint8List passphrase = utf8.encode(passphraseUtf8);
Uint8List key = generateKey(salt, passphrase);

Uint8List ciphertext = base64.decode(ciphertextBase64);
Uint8List iv = hex.decode(ivHex);
Uint8List decrypted = decrypt(ciphertext, key, iv);

print(utf8.decode(decrypted)); // This is working

the ciphertext can be decrypted to: This is working.

An alternative to PointyCastle is the cryptography package, which allows even a more compact implementation in the current case:

import 'package:cryptography/cryptography.dart';
import 'package:convert/convert.dart';
import 'dart:convert';
import 'dart:typed_data';
...
static Uint8List decrypt(Uint8List ciphertext, Uint8List key, Uint8List iv) {
  SecretKey secretKey = new SecretKey(key);
  Nonce nonce = new Nonce(iv);
  Uint8List decrypted = aesCbc.decryptSync(ciphertext, secretKey: secretKey, nonce: nonce);
  return decrypted;
}

static Uint8List generateKey(Uint8List salt, Uint8List passphrase){
  Pbkdf2 pbkdf2 = Pbkdf2(macAlgorithm: new Hmac(sha1), iterations: 5, bits: 128);
  return pbkdf2.deriveBitsSync(passphrase, nonce: Nonce(salt));
}

Note that in practice IV and salt must be generated randomly for each encryption (which you already mentioned in your question). Apart from that the iteration count of 5 is generally much too low.

Share:
3,784
Mayur_Thakur
Author by

Mayur_Thakur

Updated on December 24, 2022

Comments

  • Mayur_Thakur
    Mayur_Thakur over 1 year

    I am already having encryption code in java. Now I want to consume APIs from my server. Even after trying various tutorials and sample codes I am not able to successfully decrypt the hash.

    I know fixed salt and IV is not recommended at all. But for simplicity and to understand the issue I have kept salt and IV to "00000000000000000000000000000000";

    Hash After Encryption from Java = "XjxCg0KK0ZDWa4XMFhykIw=="; Private key used = "Mayur12354673645"

    Can someone please help me to decrypt above string using dart.

    JAVA Code

    public String encrypt(String salt, String iv, String passphrase,
                                  String plaintext) {
                try {
                    SecretKey key = generateKey(salt, passphrase);
                    byte[] encrypted = doFinal(Cipher.ENCRYPT_MODE, key, iv, plaintext
                            .getBytes("UTF-8"));
                    return base64(encrypted);
                } catch (UnsupportedEncodingException e) {
                    throw fail(e);
                }
            }
        
            public String decrypt(String salt, String iv, String passphrase,
                                  String ciphertext) {
                try {
                    SecretKey key = generateKey(salt, passphrase);
                    byte[] decrypted = doFinal(Cipher.DECRYPT_MODE, key, iv,
                            base64(ciphertext));
                    return new String(decrypted, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    throw fail(e);
                }
            }    
        
        private SecretKey generateKey(String salt, String passphrase) {
                    try {
                        SecretKeyFactory factory = SecretKeyFactory
                                .getInstance("PBKDF2WithHmacSHA1");
                        KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), hex(salt),
                                iterationCount, keySize);
                        SecretKey key = new SecretKeySpec(factory.generateSecret(spec)
                                .getEncoded(), "AES");
                        return key;
                    } catch (Exception e) {
                        e.printStackTrace();
                        return null;
                    }
                }
    
    private byte[] doFinal(int encryptMode, SecretKey key, String iv,
                               byte[] bytes) {
            try {
                cipher.init(encryptMode, key, new IvParameterSpec(hex(iv)));
                return cipher.doFinal(bytes);
            } catch (Exception e) {
                e.printStackTrace();
                throw fail(e);
            }
        }
    

    My Dart Code

    import 'package:pointycastle/block/aes_fast.dart';
    import 'package:pointycastle/block/modes/cbc.dart';
    import 'package:pointycastle/digests/sha1.dart';
    import 'package:pointycastle/key_derivators/pbkdf2.dart';
    import 'package:pointycastle/macs/hmac.dart';
    import 'package:pointycastle/paddings/pkcs7.dart';
    import 'package:pointycastle/pointycastle.dart';
    import 'dart:convert';
    import 'dart:typed_data';
    import 'package:convert/convert.dart';
    import 'dart:developer';
    
    import 'package:pointycastle/random/fortuna_random.dart';
    
    const KEY_SIZE = 16;
    const ITERATION_COUNT = 5;
    
    class EncryptionHandler {
      static const CBC_MODE = 'CBC';
    
      static Uint8List deriveKey(dynamic password,
          {String salt = '0000000000000000',
          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);
        String hexSalt = formatBytesAsHexString(saltBytes);
    
        KeyDerivator keyDerivator =
        new PBKDF2KeyDerivator(new HMac(new SHA1Digest(), 64));
    
        Pbkdf2Parameters params =
            new Pbkdf2Parameters(saltBytes, iterationCount, derivedKeyLength);
    
        keyDerivator.init(params);
    
        return keyDerivator.process(password);
      }
    
      Uint8List createUint8ListFromHexString(String hex) {
        var result = new Uint8List(hex.length ~/ 2);
        for (var i = 0; i < hex.length; i += 2) {
          var num = hex.substring(i, i + 2);
          var byte = int.parse(num, radix: 16);
          result[i ~/ 2] = byte;
        }
        return result;
      }
    
      static String formatBytesAsHexString(Uint8List bytes) {
        var result = new StringBuffer();
        for (var i = 0; i < bytes.lengthInBytes; i++) {
          var part = bytes[i];
          result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
        }
        return result.toString();
      }
    
      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}) {
        Uint8List derivedKey = deriveKey(password);
        KeyParameter keyParam = new KeyParameter(derivedKey);
        BlockCipher aes = new AESFastEngine();
    
        var rnd = FortunaRandom();
        rnd.seed(keyParam);
        Uint8List iv = createUint8ListFromString("0000000000000000");
    
        BlockCipher cipher;
        ParametersWithIV params = new ParametersWithIV(keyParam, iv);
        cipher = new CBCBlockCipher(aes);
        cipher.init(true, params);
    
        Uint8List textBytes = createUint8ListFromString(plaintext);
        Uint8List paddedText = pad(textBytes, aes.blockSize);
        Uint8List cipherBytes = _processBlocks(cipher, paddedText);
        Uint8List cipherIvBytes = new Uint8List(cipherBytes.length + iv.length)
          ..setAll(0, iv)
          ..setAll(iv.length, cipherBytes);
    
        return base64.encode(cipherIvBytes);
      }
    
      static String decrypt(String password, String ciphertext) {
        log('Password: $password');
        Uint8List derivedKey = deriveKey(password);
        log('derivedKey: $derivedKey');
        KeyParameter keyParam = new KeyParameter(derivedKey);
        log('keyParam: $keyParam');
        BlockCipher aes = new AESFastEngine();
    
        Uint8List cipherIvBytes = base64.decode(ciphertext);
        log('cipherIvBytes: $cipherIvBytes');
        Uint8List iv = createUint8ListFromString("0000000000000000");
        // Uint8List iv = new Uint8List(aes.blockSize)
        //   ..setRange(0, aes.blockSize, cipherIvBytes);
        log('iv: $iv');
        BlockCipher cipher;
        ParametersWithIV params = new ParametersWithIV(keyParam, iv);
        log('params: $params');
        cipher = new CBCBlockCipher(aes);
        log('cipher: $cipher');
        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);
        log('cipher: $paddedText');
        Uint8List textBytes = paddedText;
        // Uint8List textBytes = unpad(paddedText);
    
        return new String.fromCharCodes(textBytes);
      }
    
      static Uint8List createUint8ListFromString(String s) {
        var ret = new Uint8List(s.length);
        for (var i = 0; i < s.length; i++) {
          ret[i] = s.codeUnitAt(i);
        }
        return ret;
      }
    
      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;
      }
    }
    
  • Mayur_Thakur
    Mayur_Thakur over 3 years
    Thank you so much for giving your valuable time to help me. This sample works like charm. For salt and iv, yes this will be dynamic and i will check the iteration count too.