bcgit / pc-dart

Pointy Castle - Dart Derived Bouncy Castle APIs
MIT License
241 stars 125 forks source link

AES-GCM - 3 distinct decryption/encryption issues in processBytes(), if called multiple times (e.g. for stream) before doFinal() #182

Open tomekit opened 1 year ago

tomekit commented 1 year ago

I've implemented streamed AES-GCM encryption/decryption. That means I am required to call: processBytes() with chunks of data, then I will call doFinal() to flush the remaining buffer and generate/verify MAC.

There are multiple issues which I've managed to address, changes are pretty tiny. They are in my fork: https://github.com/bcgit/pc-dart/compare/master...tomekit:pc-dart:master

If you find them useful, please go ahead and merge them upstream. I am adding more internal unit tests, however I've limited ability to confirm it's not breaking anything within PointyCastle. If you need more input on these issues or fix, please let me know.

Issue 1 Encrypted cipherText doesn't correspond to input. Some bytes are overwritten by previous contents of _bufBlock.

In a below example I am trying to encrypt: abcdefghijklmnop-qrstuvwxyz123456 whereas, this ends up being encrypted: abcdefghijklmnop-qrstuvwxyz12345q

Example code to reproduce this:

import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/block/aes.dart';
import 'package:pointycastle/block/modes/gcm.dart';
import "package:hex/hex.dart";

void main() async {
  const TAG_LENGTH = 16;
  const BLOCK_SIZE = 16;
  final key = hexToUint8List('88fe0ff8c4eaf468d4cd9d9a9831662488fe0ff8c4eaf468d4cd9d9a98316624');
  final nonce = hexToUint8List('3c95422167063c9542216706');

  final plaintextPart1 = Uint8List.fromList(utf8.encode("abcdefghijklmnop-"));
  final plaintextPart2 = Uint8List.fromList(utf8.encode("qrstuvwxyz123456"));

  final cipher = GCMBlockCipher(AESEngine());

  cipher.init(true, AEADParameters(KeyParameter(key), TAG_LENGTH * 8, nonce, Uint8List(0)));

  var bufferOut = Uint8List(plaintextPart1.length);
  final bytesProcessed = cipher.processBytes(plaintextPart1, 0, plaintextPart1.length, bufferOut, 0);
  bufferOut = bufferOut.sublist(0, bytesProcessed);

  var bufferOut2 = Uint8List(plaintextPart2.length);
  final bytesProcessed2 = cipher.processBytes(plaintextPart2, 0, plaintextPart2.length, bufferOut2, 0);
  bufferOut2 = bufferOut2.sublist(0, bytesProcessed2);

  var bufferOut3 = Uint8List(TAG_LENGTH + BLOCK_SIZE);
  final bytesProcessed3 = cipher.doFinal(bufferOut3, 0);
  bufferOut3 = bufferOut3.sublist(0, bytesProcessed3);

  final cipherTextWithMac = bufferOut + bufferOut2 + bufferOut3;
  print("Ciphertext: " + HEX.encode(cipherTextWithMac.sublist(0, cipherTextWithMac.length - TAG_LENGTH))); // f0c2b9571faa064c7b730143ef1a8699e871859250fcac171571ee56639d9c9644
  print("MAC: " + HEX.encode(cipherTextWithMac.sublist(cipherTextWithMac.length - TAG_LENGTH))); // ba49063f7908d98cbb69df8ead55973d

  // DECRYPT
  final decryptCipher = GCMBlockCipher(AESEngine());
  decryptCipher.init(false, AEADParameters(KeyParameter(key), TAG_LENGTH * 8, nonce, Uint8List(0)));
  final decryptedPlainText = decryptCipher.process(Uint8List.fromList(cipherTextWithMac));

  print(" PRE: " + utf8.decode(plaintextPart1 + plaintextPart2));
  print("POST: " + utf8.decode(decryptedPlainText));

// FAIL:
// abcdefghijklmnop-qrstuvwxyz123456
// !=
// abcdefghijklmnop-qrstuvwxyz12345q

   assert(utf8.decode(plaintextPart1) + utf8.decode(plaintextPart2) == utf8.decode(decryptedPlainText));
}

Uint8List hexToUint8List(String hex) {
  if (!(hex is String)) {
    throw 'Expected string containing hex digits';
  }
  if (hex.length % 2 != 0) {
    throw 'Odd number of hex digits';
  }
  var l = hex.length ~/ 2;
  var result = new Uint8List(l);
  for (var i = 0; i < l; ++i) {
    var x = int.parse(hex.substring(i * 2, (2 * (i + 1))), radix: 16);
    if (x.isNaN) {
      throw 'Expected hex string';
    }
    result[i] = x;
  }
  return result;
}

Issue 2. During decryption, for some inputs, buffer size returned by getOutputSize is too small. In some cases there might be up to 16 bytes coming from an internal buffer: _bufBlock. Change in: getOutputSize was required.

Issue 3. Decryption doesn't happen until input exceeds 16 bytes (e.g. 17 bytes). Fix. "base_aead_block_cipher.dart:194", change from: while (len > blockSize) to while (len >= blockSize)