You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AES decrypts a zero-byte ciphertext "successfully" to empty plaintext through Decrypt(byte[]) and the stream paths, though the span TryDecrypt rejects it #75
Essentials.EncryptionProviders.Aes/AesEncryptionProvider.cs treats an empty ciphertext differently depending on which overload you call:
The span TryDecrypt(ReadOnlySpan<byte> data, ...) (line ~223) returns false when data.IsEmpty.
The stream TryDecrypt(Stream, ...) (line ~268) and TryDecryptAsync(Stream, ...) (line ~318) have no such check. CryptoStream reads nothing and they return true.
The default IEncryptionProvider.Decrypt(ReadOnlySpan<byte>, ...) and TryDecrypt(span, ..., Stream) in Essentials/IEncryptionProvider.cs go through the stream path via a MemoryStream, so they inherit the lenient behaviour.
TryDecrypt(empty span, key, iv, Span<byte>) -> False
aes.Decrypt(Array.Empty<byte>(), key, iv) -> byte[0], no exception
TryDecrypt(new MemoryStream(), key, iv, dest) -> True, 0 bytes written
Why it matters
AES-CBC with PKCS7 padding always produces at least one 16-byte block, so a zero-byte ciphertext is never valid. A file truncated to zero bytes, or an empty payload from a failed download, is reported as a valid decryption of an empty plaintext. The caller then stores or acts on empty data with no error. A truncation to 20 bytes, by contrast, is correctly rejected. The empty case is the one that slips through.
Suggested fix
Make every decrypt path reject a ciphertext with no bytes, e.g. count bytes read from the source (wrap it in a counting stream, or check CanSeek && Length - Position == 0 up front), and return false/throw when nothing was read. Alternatively, have the default Decrypt(ReadOnlySpan<byte>, ...) call the span TryDecrypt rather than routing through streams.
Acceptance:Decrypt(Array.Empty<byte>(), key, iv) throws, and TryDecrypt/TryDecryptAsync over an empty stream return false, matching the span overload.
Priority: Medium. A truncated-to-zero file or an empty download "decrypts" to empty plaintext, and the caller acts on it without any error. The path a caller takes depends on which overload they use, so the span and stream APIs disagree. This fails open rather than exposing data, and callers who use the span TryDecrypt are already protected, so it isn't High.
Area / suggested assignment:Essentials.EncryptionProviders.Aes (the stream TryDecrypt/TryDecryptAsync) and the default IEncryptionProvider.Decrypt in Essentials/IEncryptionProvider.cs. The simpler fix is to route the default span Decrypt through the span TryDecrypt.
Activity
matt-edmondson commentedon Sep 30, 2026
Triage
TryDecryptare already protected, so it isn't High.Essentials.EncryptionProviders.Aes(the streamTryDecrypt/TryDecryptAsync) and the defaultIEncryptionProvider.DecryptinEssentials/IEncryptionProvider.cs. The simpler fix is to route the default spanDecryptthrough the spanTryDecrypt.Generated by Claude Code