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.
Summary
The native FFI AES-CBC
decryptStreamimplementation 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 anOperationError.Chrome rejects the same input without emitting any plaintext.
Reproduction
Observed results:
OperationErrorwhenEVP_CipherFinal_exrejects the invalid padding.OperationErrorwithout 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_CipherUpdateand emit it only afterEVP_CipherFinal_exvalidates the final padding. Keep encryption incremental. Add a cross-backend regression that corrupts the final ciphertext byte and asserts thatdecryptStreamemits no chunks before rejection.