FiloSottile/age · error
failed to read final chunk
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.
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
- non-EOF error reading after end of encrypted file
- offset out of range [0: ]
- failed to decrypt and authenticate chunk at offset
- failed to decrypt and authenticate final chunk
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/875a771df128d662.
Report an issue: GitHub.
Appendix: 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)