internal/stream: detect trailing data returned alongside io.EOF

The trailing data check after a full-length final chunk ignored the
byte count, so a reader returning (1, io.EOF), as io.Reader permits,
would have its trailing data silently accepted, and a transient
(0, nil) return would be misreported as trailing data. io.ReadFull
handles both.
This commit is contained in:
Filippo Valsorda
2026-08-29 14:31:26 +02:00
parent 91bc452b9a
commit c083f6045f
2 changed files with 44 additions and 1 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ func (r *DecryptReader) Read(p []byte) (int, error) {
// words, check for trailing data after a full-length final chunk.
// Hopefully, the underlying reader supports returning EOF even if it
// had previously returned an EOF to ReadFull.
if _, err := r.src.Read(make([]byte, 1)); err == nil {
if _, err := io.ReadFull(r.src, make([]byte, 1)); err == nil {
r.err = errors.New("trailing data after end of encrypted file")
} else if err != io.EOF {
r.err = fmt.Errorf("non-EOF error reading after end of encrypted file: %w", err)
+43
View File
@@ -451,6 +451,49 @@ func TestDecryptReaderAtEOF(t *testing.T) {
}
}
// TestDecryptReaderTrailingData checks that data appended after a full-length
// final chunk is rejected, including when the source returns the trailing byte
// alongside io.EOF as permitted by io.Reader.
func TestDecryptReaderTrailingData(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
rand.Read(key)
// A plaintext of exactly one full chunk, so the trailing data can only be
// detected by reading past the final chunk.
plaintext := make([]byte, cs)
rand.Read(plaintext)
ciphertext := encrypt(t, key, plaintext)
trailing := append(bytes.Clone(ciphertext), 0x42)
wrappers := map[string]func(io.Reader) io.Reader{
"plain": func(r io.Reader) io.Reader { return r },
"dataErr": iotest.DataErrReader,
}
for name, wrap := range wrappers {
t.Run(name, func(t *testing.T) {
r, err := stream.NewDecryptReader(key, wrap(bytes.NewReader(trailing)))
if err != nil {
t.Fatal(err)
}
if _, err := io.ReadAll(r); err == nil {
t.Error("trailing data: expected error, got nil")
}
r, err = stream.NewDecryptReader(key, wrap(bytes.NewReader(ciphertext)))
if err != nil {
t.Fatal(err)
}
got, err := io.ReadAll(r)
if err != nil {
t.Errorf("valid file: got err=%v, want nil", err)
}
if !bytes.Equal(got, plaintext) {
t.Error("valid file: plaintext mismatch")
}
})
}
}
func TestDecryptReaderAtEmpty(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
rand.Read(key)