FiloSottile/age · error

failed to decrypt and authenticate chunk at offset %d: %w

Error message

failed to decrypt and authenticate chunk at offset %d: %w

What it means

DecryptReaderAt.ReadAt read a chunk successfully, but ChaCha20Poly1305 authentication failed when opening it. Because each chunk carries its own AEAD tag, this means the bytes at that chunk offset are not the authentic ciphertext for this key and chunk index. In age's threat model this indicates corruption or tampering, and the read is aborted with any bytes already copied returned as n.

Source

Thrown at internal/stream/stream.go:443

		chunkSize := min(encSize-chunkOff, encChunkSize)

		cached := r.cache.Load()
		var plaintext []byte
		if cached != nil && cached.off == chunkOff {
			plaintext = cached.data
			cacheUpdate = nil
		} else {
			if err := readFullAt(r.src, chunk[:chunkSize], chunkOff); err != nil {
				return n, fmt.Errorf("failed to read chunk at offset %d: %w", chunkOff, err)
			}
			nonce := nonceForChunk(chunkIndex)
			if chunkIndex == r.chunks-1 {
				setLastChunkFlag(nonce)
			}
			var err error
			plaintext, err = r.a.Open(chunk[:0], nonce[:], chunk[:chunkSize], nil)
			if err != nil {
				return n, fmt.Errorf("failed to decrypt and authenticate chunk at offset %d: %w", chunkOff, err)
			}
			cacheUpdate = &cachedChunk{off: chunkOff, data: plaintext}
		}

		plainChunkOff := int(off - chunkIndex*ChunkSize)
		copySize := min(len(plaintext)-plainChunkOff, len(p))
		copy(p, plaintext[plainChunkOff:plainChunkOff+copySize])
		p = p[copySize:]
		off += int64(copySize)
		n += copySize
	}
	if cacheUpdate != nil {
		r.cache.Store(cacheUpdate)
	}
	if off == r.size {
		return n, io.EOF
	}
	return n, nil

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Confirm the key matches the recipient this file was encrypted for.
  2. Take a snapshot/immutable view of the ciphertext while reading; do not modify or re-encrypt the file concurrently.
  3. Verify file integrity (checksum) and restore from a known-good copy if corrupted.
  4. Ensure the size used at construction still matches the current source; a changed file invalidates chunk offsets.
Defensive patterns

Strategy: try-catch

Try / catch

n, err := dr.ReadAt(p, off)
if err != nil && !errors.Is(err, io.EOF) {
    if strings.Contains(err.Error(), "failed to decrypt and authenticate chunk") {
        return fmt.Errorf("ciphertext chunk corrupted or wrong key: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: ReadAt(p, off) where aead.Open fails for the chunk at chunkOff: wrong key; ciphertext bytes for that chunk modified; the src content changed after NewDecryptReaderAt validated the final chunk (validation only authenticated the last chunk, not all chunks).

Common situations: A file being rewritten in place after opening the reader (e.g. rsync or atomic replace of the target); bit rot on storage; decrypting with the wrong identity; in-place edits by tools unaware of the format.

Understand the failure class

Related errors


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