FiloSottile/age · error
failed to read final chunk: %w
Error message
failed to read final chunk: %w
What it means
NewDecryptReaderAt validates an encrypted payload by reading and authenticating the final chunk via ReadAt. This error wraps a failure to read those ciphertext bytes from the io.ReaderAt: the read returned an error, or fewer bytes than expected (io.ErrUnexpectedEOF from readFullAt). The size was accepted by EncryptedChunkCount, but the source cannot actually supply the final chunk's bytes.
Source
Thrown at internal/stream/stream.go:396
}
func NewDecryptReaderAt(key []byte, src io.ReaderAt, size int64) (*DecryptReaderAt, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
// Check that size is valid by decrypting the final chunk.
chunks, err := EncryptedChunkCount(size)
if err != nil {
return nil, err
}
finalChunkIndex := chunks - 1
finalChunkOff := finalChunkIndex * encChunkSize
finalChunkSize := size - finalChunkOff
finalChunk := make([]byte, finalChunkSize)
if err := readFullAt(src, finalChunk, finalChunkOff); err != nil {
return nil, fmt.Errorf("failed to read final chunk: %w", err)
}
nonce := nonceForChunk(finalChunkIndex)
setLastChunkFlag(nonce)
plaintext, err := aead.Open(finalChunk[:0], nonce[:], finalChunk, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt and authenticate final chunk: %w", err)
}
cache := &cachedChunk{off: finalChunkOff, data: plaintext}
plaintextSize := size - chunks*chacha20poly1305.Overhead
r := &DecryptReaderAt{a: aead, src: src, size: plaintextSize, chunks: chunks}
r.cache.Store(cache)
return r, nil
}
func (r *DecryptReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
if off < 0 || off > r.size {
return 0, fmt.Errorf("offset out of range [0:%d]: %d", r.size, off)View on GitHub (pinned to b74dce4cdb)
Solutions
- Verify src actually contains at least size bytes: bytes.NewReader(data) with len(data) >= size, or the file's real size matches the passed size.
- Ensure the size argument and the src refer to the same ciphertext (same file, not a stale stat).
- If you implemented io.ReaderAt, confirm it returns io.EOF when reads go past the end and (n, err) per the interface contract.
- Re-obtain the ciphertext if it was truncated during transfer; NewDecryptReaderAt deliberately reads the tail to detect this early.
Example fix
// before r, err := stream.NewDecryptReaderAt(key, f, declaredSize) // declaredSize > actual file size // after info, _ := f.Stat() r, err := stream.NewDecryptReaderAt(key, f, info.Size())
Defensive patterns
Strategy: validation
Validate before calling
info, err := os.Stat(path)
if err != nil { return err }
if info.Size() != declaredSize {
return fmt.Errorf("size mismatch: declared %d, actual %d", declaredSize, info.Size())
} Try / catch
r, err := stream.NewDecryptReaderAt(key, src, size)
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
return fmt.Errorf("ciphertext shorter than declared size %d", size)
}
return err
} Prevention
- Pass the size obtained from the same source you hand to the reader (same file/handle).
- Avoid custom io.ReaderAt implementations unless they fully honor the contract (EOF semantics).
- Always construct via NewDecryptReaderAt before random-access reads; it validates the tail early.
When it happens
Trigger: NewDecryptReaderAt(key, src, size) where src.ReadAt at offset (chunks-1)*65552 fails or short-reads: the ReaderAt is shorter than size; the implementation returns a spurious error; or the implementation violates the io.ReaderAt contract.
Common situations: Passing a size larger than the actual data backing the ReaderAt (e.g. stat size from a different file, or size includes metadata); mmap-backed or bytes.Reader sources that were truncated; a custom ReaderAt that returns nil error on short read.
Related errors
- failed to read chunk at offset %d: %w
- non-EOF error reading after end of encrypted file: %w
- offset out of range [0:%d]: %d
- invalid encrypted payload size: %d
- failed to decrypt and authenticate final chunk: %w
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/875a771df128d662.
Report an issue: GitHub.