Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
tomekit opened this issue Dec 23, 2022 · 0 comments

Comments

@tomekit
Copy link

tomekit commented Dec 23, 2022

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: 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)

@tomekit tomekit changed the title AES-GCM stream - malformed cipherText when processBytes() called multiple times before doFinal() AES-GCM - multiple issues in processBytes(), if called multiple times (e.g. for stream) before doFinal() Dec 26, 2022
@tomekit tomekit changed the title AES-GCM - multiple issues in processBytes(), if called multiple times (e.g. for stream) before doFinal() AES-GCM - 3 distinct decryption/encryption issues in processBytes(), if called multiple times (e.g. for stream) before doFinal() Dec 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant