FiloSottile/age · error

failed to decrypt and authenticate final chunk: %w

Error message

failed to decrypt and authenticate final chunk: %w

What it means

NewDecryptReaderAt authenticates the final chunk with ChaCha20Poly1305 using the last-chunk nonce. This error means the final chunk's ciphertext failed AEAD verification: the data is not the authentic final chunk for this key. Unlike error [73], the bytes were read successfully but decryption/authentication failed, which age treats as corruption or tampering (or a wrong key).

Source

Thrown at internal/stream/stream.go:402

	}

	// 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)
	}
	if len(p) == 0 {
		return 0, nil
	}
	var cacheUpdate *cachedChunk
	chunk := make([]byte, encChunkSize)

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Confirm you are using the correct key for this recipient/file; wrong keys surface as authentication failure.
  2. Verify the ciphertext is byte-for-byte intact (checksum against the source of truth); re-download or restore if corrupted.
  3. Ensure the size argument matches this exact ciphertext version; a mismatched size shifts the final-chunk offset and guarantees failure.
  4. If files come from a mixed toolchain, re-encrypt with the same age STREAM implementation to rule out incompatibilities.

Example fix

// before
key := deriveKey(wrongPassphrase)
r, err := stream.NewDecryptReaderAt(key, f, size)
// after
key := deriveKey(correctPassphrase) // verify key provenance
r, err := stream.NewDecryptReaderAt(key, f, size)
Defensive patterns

Strategy: try-catch

Try / catch

r, err := stream.NewDecryptReaderAt(key, src, size)
if err != nil {
    if strings.Contains(err.Error(), "failed to decrypt and authenticate final chunk") {
        return fmt.Errorf("wrong key or corrupted ciphertext: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: NewDecryptReaderAt(key, src, size) where aead.Open on the final chunk (nonce = chunk index with last-chunk flag) fails: wrong key, wrong size causing the wrong 'final chunk' window, or ciphertext modified in place.

Common situations: Decrypting with the wrong key/identity; the ciphertext file was edited, corrupted in transit, or re-padded; the file was produced by a different scheme or a different final-chunk convention; using a stale size with a rewritten file.

Understand the failure class

Related errors


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