FiloSottile/age · error
last chunk is empty, try age v1.0.0, and please consider rep
Error message
last chunk is empty, try age v1.0.0, and please consider reporting this
What it means
During chunk decryption, if the underlying read returned io.ErrUnexpectedEOF and the resulting decrypted chunk is empty (only the 16-byte overhead) but the nonce is not zero — i.e. it is not the first chunk — the message is malformed: an encrypted stream cannot end on an empty non-initial chunk. The error hints at age v1.0.0, which had a bug writing empty final chunks.
Source
Thrown at internal/stream/stream.go:132
// readChunk reads the next chunk of ciphertext from r.src and makes it available
// in r.unread. last is true if the chunk was marked as the end of the message.
// readChunk must not be called again after returning a last chunk or an error.
func (r *DecryptReader) readChunk() (last bool, err error) {
if len(r.unread) != 0 {
panic("stream: internal error: readChunk called with dirty buffer")
}
in := r.buf[:]
n, err := io.ReadFull(r.src, in)
switch {
case err == io.EOF:
// A message can't end without a marked chunk. This message is truncated.
return false, io.ErrUnexpectedEOF
case err == io.ErrUnexpectedEOF:
// The last chunk can be short, but not empty unless it's the first and
// only chunk.
if !nonceIsZero(&r.nonce) && n == r.a.Overhead() {
return false, errors.New("last chunk is empty, try age v1.0.0, and please consider reporting this")
}
in = in[:n]
last = true
setLastChunkFlag(&r.nonce)
case err != nil:
return false, err
}
outBuf := make([]byte, 0, ChunkSize)
out, err := r.a.Open(outBuf, r.nonce[:], in, nil)
if err != nil && !last {
// Check if this was a full-length final chunk.
last = true
setLastChunkFlag(&r.nonce)
out, err = r.a.Open(outBuf, r.nonce[:], in, nil)
}
if err != nil {
return false, errors.New("failed to decrypt and authenticate payload chunk, file may be corrupted or tampered with")View on GitHub (pinned to b74dce4cdb)
Solutions
- Try decrypting with age v1.0.0, which tolerates empty last chunks (the message says so explicitly).
- Re-encrypt the plaintext with a current age version and redistribute the file.
- If truncation is suspected, re-transfer the ciphertext and compare checksums with the sender.
Example fix
// before (decrypting with current age) // fails on age v1.0.0 ciphertexts with empty last chunk // after // decrypt with age v1.0.0 binary, then re-encrypt: // age-v1.0.0 -d -i key.txt file.age > plain && age -r recipient > file.age < plain
Defensive patterns
Strategy: try-catch
Try / catch
if _, err := io.Copy(out, r); err != nil {
if strings.Contains(err.Error(), "last chunk is empty") {
return fmt.Errorf("file produced by buggy age v1.0.0: %w", err)
}
return err
} Prevention
- Keep age updated to a version newer than v1.0.0 for both encrypting and decrypting parties.
- Re-encrypt any legacy ciphertext created with v1.0.0.
- Compare file checksums with the sender to detect truncation.
When it happens
Trigger: stream.Reader.readChunk hits io.ErrUnexpectedEOF while reading a chunk whose plaintext size equals only the AEAD overhead (empty chunk) and the chunk nonce is non-zero (not the first chunk).
Common situations: Decrypting files produced by age v1.0.0 which could emit an empty last chunk; ciphertext truncated at a chunk boundary after the first chunk.
Related errors
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/7b71446ba30bf203.
Report an issue: GitHub.