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

  1. Try decrypting with age v1.0.0, which tolerates empty last chunks (the message says so explicitly).
  2. Re-encrypt the plaintext with a current age version and redistribute the file.
  3. 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

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.