FiloSottile/age · critical

failed to decrypt and authenticate payload chunk, file may…

Error message

failed to decrypt and authenticate payload chunk, file may be corrupted or tampered with

What it means

The ChaCha20Poly1305 AEAD Open call failed on a payload chunk, meaning authentication (MAC) or decryption failed. Since the header MAC already passed, this means the encrypted payload was modified, corrupted, or encrypted with a different file key. The reader treats any single bad chunk as a fatal error for the whole file.

Solutions

  1. Confirm you are using the correct identity/private key that the file was encrypted for.
  2. Re-download or re-copy the file and verify its checksum against the sender.
  3. Re-encrypt from the original plaintext if the source is corrupted beyond repair.
  4. Check that no transport step (FTP ascii mode, editor save) mangled the binary ciphertext.

Example fix

// before
r, _ := age.Decrypt(input, wrongIdentity) // payload chunks fail auth
// after
r, err := age.Decrypt(input, correctIdentity)
if err != nil { return fmt.Errorf("decrypt: %w", err) }
Defensive patterns

Strategy: try-catch

Try / catch

if _, err := io.Copy(out, r); err != nil {
    if strings.Contains(err.Error(), "failed to decrypt and authenticate payload chunk") {
        return errors.New("wrong key or corrupted/tampered file")
    }
    return err
}

Prevention

When it happens

Trigger: stream.Reader.readChunk calls r.a.Open and it returns a non-nil error — wrong identity/key (mismatched file key), bit rot, truncated bit flips, or tampered ciphertext bytes within any chunk.

Common situations: Decrypting with the wrong identity (file encrypted for someone else); corrupted downloads or damaged storage; malicious modification of the .age file; copying the file in text mode that altered bytes.

Understand the failure class

Related errors


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/a9d82a98f8141e38. Report an issue: GitHub.

Appendix: source

Thrown at internal/stream/stream.go:150

			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")
	}

	incNonce(&r.nonce)
	r.unread = r.buf[:copy(r.buf[:], out)]
	return last, nil
}

func incNonce(nonce *[chacha20poly1305.NonceSize]byte) {
	for i := len(nonce) - 2; i >= 0; i-- {
		nonce[i]++
		if nonce[i] != 0 {
			return
		}
	}
	// The counter is 88 bits, this is unreachable.
	panic("stream: chunk counter wrapped around")
}

View on GitHub (pinned to b74dce4cdb)