dartencryptionaespkcs#7pointycastle

AES CBC encryption with PKCS7 padding in Dart (Flutter): pkcs7.dart throws an error when the encrypted text does not require padding


I am trying to encrypt some text with AES CBC encryption with PKCS7 padding in Python and then decrypt it in Dart (Flutter). It turns out package:pointycastle/paddings/pkcs7.dart throws an error when the encrypted text does not require padding.

Here is the example. Here we encode some text that requires padding in Python.

import base64
import hashlib
from Crypto.Cipher import AES
from pkcs7 import PKCS7Encoder

secret_text = 'Text that requires padding'

key_text = "some_key"
key = hashlib.sha256(key_text.encode()).digest()
print('key: ', list(key))

iv = hashlib.md5("some_iv_text".encode('utf-8')).digest()
print('iv: ', list(iv))

encoder = PKCS7Encoder()
padded_text = encoder.encode(secret_text)
print('Padded text: ', padded_text.encode('utf-8'))

e = AES.new(key, AES.MODE_CBC, iv)
cipher_text = e.encrypt(padded_text.encode('utf-8'))

print(base64.b64encode(cipher_text).decode('utf-8'))

Output:

key:  [149, 37, 31, 220, 159, 221, 63, 146, 231, 174, 209, 65, 254, 127, 208, 107, 42, 235, 157, 237, 182, 247, 172, 8, 231, 217, 186, 188, 126, 137, 139, 73]
iv:  [205, 35, 149, 118, 23, 255, 66, 222, 206, 81, 138, 188, 169, 122, 96, 164]
Padded text:  b'Text that requires padding\x06\x06\x06\x06\x06\x06'
PygLEjc1Kkjm5qtt5N9vHC0E7TAYVj4xLpsfa4cLiRs=

Process finished with exit code 0

Next, we decrypt it in Dart.

import 'dart:typed_data';
import 'package:encrypt/encrypt.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart';

void main() {
  String encryptedText = 'PygLEjc1Kkjm5qtt5N9vHC0E7TAYVj4xLpsfa4cLiRs=';

  List<int> key_text = utf8.encode("some_key"); // data being hashed
  List<int> key_bytes = sha256.convert(key_text).bytes;

  final key = Key(Uint8List.fromList(key_bytes));

  print('key: $key_bytes');

  Encrypter encrypter =
      Encrypter(AES(key, mode: AESMode.cbc, padding: 'PKCS7'));

  String iv_text = 'some_iv_text';

  Uint8List encrypted = Uint8List.fromList(base64.decode(encryptedText));

  final iv_bytes = md5.convert(utf8.encode(iv_text)).bytes;
  IV iv = IV(Uint8List.fromList(iv_bytes));

  print('iv: $iv_bytes');

  String decrypted = encrypter.decrypt(Encrypted(encrypted), iv: iv);
  print(decrypted);
}

Output:

key: [149, 37, 31, 220, 159, 221, 63, 146, 231, 174, 209, 65, 254, 127, 208, 107, 42, 235, 157, 237, 182, 247, 172, 8, 231, 217, 186, 188, 126, 137, 139, 73]
iv: [205, 35, 149, 118, 23, 255, 66, 222, 206, 81, 138, 188, 169, 122, 96, 164]
Text that requires padding

Process finished with exit code 0

So, everything works well.

Let us do the same with the text that does not require padding.

import base64
import hashlib
from Crypto.Cipher import AES
from pkcs7 import PKCS7Encoder

secret_text = 'Text that does not require padding..............'

key_text = "some_key"
key = hashlib.sha256(key_text.encode()).digest()
print('key: ', list(key))

iv = hashlib.md5("some_iv_text".encode('utf-8')).digest()
print('iv: ', list(iv))

encoder = PKCS7Encoder()
padded_text = encoder.encode(secret_text)
print('Padded text: ', padded_text.encode('utf-8'))

e = AES.new(key, AES.MODE_CBC, iv)
cipher_text = e.encrypt(padded_text.encode('utf-8'))

print(base64.b64encode(cipher_text).decode('utf-8'))

Output:

key:  [149, 37, 31, 220, 159, 221, 63, 146, 231, 174, 209, 65, 254, 127, 208, 107, 42, 235, 157, 237, 182, 247, 172, 8, 231, 217, 186, 188, 126, 137, 139, 73]
iv:  [205, 35, 149, 118, 23, 255, 66, 222, 206, 81, 138, 188, 169, 122, 96, 164]
Padded text:  b'Text that does not require padding..............'
W2Drr/UfPBTNG4fVPG0Lb/Ax3GAyvjNNx9BlrJcxc2RTNcf2BxT2lyIx1l2ktOYl

Process finished with exit code 0

Decrypting in Dart with the same code:

import 'dart:typed_data';
import 'package:encrypt/encrypt.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart';

void main() {
  String encryptedText = 'W2Drr/UfPBTNG4fVPG0Lb/Ax3GAyvjNNx9BlrJcxc2RTNcf2BxT2lyIx1l2ktOYl';

  List<int> key_text = utf8.encode("some_key"); // data being hashed
  List<int> key_bytes = sha256.convert(key_text).bytes;

  final key = Key(Uint8List.fromList(key_bytes));

  print('key: $key_bytes');

  Encrypter encrypter =
      Encrypter(AES(key, mode: AESMode.cbc, padding: 'PKCS7'));

  String iv_text = 'some_iv_text';

  Uint8List encrypted = Uint8List.fromList(base64.decode(encryptedText));

  final iv_bytes = md5.convert(utf8.encode(iv_text)).bytes;
  IV iv = IV(Uint8List.fromList(iv_bytes));

  print('iv: $iv_bytes');

  String decrypted = encrypter.decrypt(Encrypted(encrypted), iv: iv);
  print(decrypted);
}

Output:

key: [149, 37, 31, 220, 159, 221, 63, 146, 231, 174, 209, 65, 254, 127, 208, 107, 42, 235, 157, 237, 182, 247, 172, 8, 231, 217, 186, 188, 126, 137, 139, 73]
Unhandled exception:
Invalid argument(s): Invalid or corrupted pad block
#0      PKCS7Padding.padCount (package:pointycastle/paddings/pkcs7.dart:42:7)
#1      PaddedBlockCipherImpl.doFinal (package:pointycastle/padded_block_cipher/padded_block_cipher_impl.dart:112:30)
#2      PaddedBlockCipherImpl.process (package:pointycastle/padded_block_cipher/padded_block_cipher_impl.dart:74:25)
#3      AES.decrypt (package:encrypt/src/algorithms/aes.dart:63:22)
#4      Encrypter.decryptBytes (package:encrypt/src/encrypter.dart:25:17)
#5      Encrypter.decrypt (package:encrypt/src/encrypter.dart:31:17)
#6      main (package:zno_ua_mova/decypher_bug_reproduce.dart:28:32)
#7      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#8      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
iv: [205, 35, 149, 118, 23, 255, 66, 222, 206, 81, 138, 188, 169, 122, 96, 164]

Process finished with exit code 255

It turns out the exception is thrown by this code block in package:pointycastle/paddings/pkcs7.dart:42:7

if (count > data.length || count == 0) {
  throw ArgumentError('Invalid or corrupted pad block');
}

When decryption works (first scenario), the variables are:

count: 6
data.length: 16

When decryption does not work (second scenario), the variables are:

count: 46
data.length: 16

Is there anything wrong with the pointycastle pkcs7 lib, or am I using it in the wrong way?


Solution

  • Ok, the problem turned out to be in the Python pkcs7 library. Its implementation does not follow the PKCS7 RFC and does not add the padding byte to the strings when its length mod blocksize equals zero. The Github issue is already there.