Skip to content

bug: native AES-CBC decryptStream emits unauthenticated plaintext before padding validation #412

Description

@harrshita123

Summary

The native FFI AES-CBC decryptStream implementation emits decrypted chunks before final PKCS#7 padding validation completes. If the final ciphertext block has invalid padding, consumers can receive plaintext and then receive an OperationError.

Chrome rejects the same input without emitting any plaintext.

Reproduction

final key = await AesCbcSecretKey.importRawKey(Uint8List(16));
final iv = Uint8List(16);
final ciphertext = await key.encryptBytes(
  Uint8List.fromList(List.generate(32, (i) => i)),
  iv,
);
ciphertext[ciphertext.length - 1] ^= 1;

final emitted = <int>[];
try {
  await for (final chunk in key.decryptStream(Stream.value(ciphertext), iv)) {
    emitted.addAll(chunk);
  }
} on OperationError {
}

expect(emitted, isNotEmpty);

Observed results:

  • Native FFI: emits plaintext, then throws OperationError when EVP_CipherFinal_ex rejects the invalid padding.
  • Chrome: throws OperationError without emitting a stream chunk.

Impact

Callers can process, persist, or expose data that should be rejected after AES-CBC decryption fails. The current behavior also differs across supported backends.

Proposed fix

For native AES-CBC decryption, buffer output from EVP_CipherUpdate and emit it only after EVP_CipherFinal_ex validates the final padding. Keep encryption incremental. Add a cross-backend regression that corrupts the final ciphertext byte and asserts that decryptStream emits no chunks before rejection.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions