{"record":{"id":"875a771df128d662","repo":"FiloSottile/age","slug":"failed-to-read-final-chunk-w","errorCode":null,"errorMessage":"failed to read final chunk: %w","messagePattern":"failed to read final chunk: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/stream/stream.go","lineNumber":396,"sourceCode":"}\n\nfunc NewDecryptReaderAt(key []byte, src io.ReaderAt, size int64) (*DecryptReaderAt, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Check that size is valid by decrypting the final chunk.\n\tchunks, err := EncryptedChunkCount(size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfinalChunkIndex := chunks - 1\n\tfinalChunkOff := finalChunkIndex * encChunkSize\n\tfinalChunkSize := size - finalChunkOff\n\tfinalChunk := make([]byte, finalChunkSize)\n\tif err := readFullAt(src, finalChunk, finalChunkOff); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read final chunk: %w\", err)\n\t}\n\tnonce := nonceForChunk(finalChunkIndex)\n\tsetLastChunkFlag(nonce)\n\tplaintext, err := aead.Open(finalChunk[:0], nonce[:], finalChunk, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decrypt and authenticate final chunk: %w\", err)\n\t}\n\tcache := &cachedChunk{off: finalChunkOff, data: plaintext}\n\n\tplaintextSize := size - chunks*chacha20poly1305.Overhead\n\tr := &DecryptReaderAt{a: aead, src: src, size: plaintextSize, chunks: chunks}\n\tr.cache.Store(cache)\n\treturn r, nil\n}\n\nfunc (r *DecryptReaderAt) ReadAt(p []byte, off int64) (n int, err error) {\n\tif off < 0 || off > r.size {\n\t\treturn 0, fmt.Errorf(\"offset out of range [0:%d]: %d\", r.size, off)","sourceCodeStart":378,"sourceCodeEnd":414,"githubUrl":"https://github.com/FiloSottile/age/blob/b74dce4cdbe35b5e5f66c06d9612b72f89028758/internal/stream/stream.go#L378-L414","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nr, err := stream.NewDecryptReaderAt(key, f, declaredSize) // declaredSize > actual file size\n// after\ninfo, _ := f.Stat()\nr, err := stream.NewDecryptReaderAt(key, f, info.Size())","handlingStrategy":"validation","validationCode":"info, err := os.Stat(path)\nif err != nil { return err }\nif info.Size() != declaredSize {\n    return fmt.Errorf(\"size mismatch: declared %d, actual %d\", declaredSize, info.Size())\n}","typeGuard":null,"tryCatchPattern":"r, err := stream.NewDecryptReaderAt(key, src, size)\nif err != nil {\n    if errors.Is(err, io.ErrUnexpectedEOF) {\n        return fmt.Errorf(\"ciphertext shorter than declared size %d\", size)\n    }\n    return err\n}","preventionTips":["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."],"tags":["stream","io","readerat","truncated-data","go"],"backgroundTag":"truncated-ciphertext","analyzedSha":"b74dce4cdbe35b5e5f66c06d9612b72f89028758","analyzedAt":"2026-08-31T23:59:31.627Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}