FiloSottile/age · error

invalid encrypted payload size: %d

Error message

invalid encrypted payload size: %d

What it means

EncryptedChunkCount validates the size of an encrypted STREAM payload. This error means the caller passed a size that cannot possibly be a valid encrypted payload: either a negative number, or a size so large the chunk count arithmetic would overflow. The library refuses to guess a chunk count from malformed sizes because the chunk structure determines the plaintext layout.

Source

Thrown at internal/stream/stream.go:25

import (
	"bytes"
	"crypto/cipher"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"math"
	"sync/atomic"

	"golang.org/x/crypto/chacha20poly1305"
)

const ChunkSize = 64 * 1024

func EncryptedChunkCount(encryptedSize int64) (int64, error) {
	if encryptedSize < 0 || encryptedSize > math.MaxInt64-encChunkSize+1 {
		return 0, fmt.Errorf("invalid encrypted payload size: %d", encryptedSize)
	}
	chunks := (encryptedSize + encChunkSize - 1) / encChunkSize

	plaintextSize := encryptedSize - chunks*chacha20poly1305.Overhead
	expChunks := (plaintextSize + ChunkSize - 1) / ChunkSize
	// Empty plaintext, the only case that allows (and requires) an empty chunk.
	if plaintextSize == 0 {
		expChunks = 1
	}
	if expChunks != chunks {
		return 0, fmt.Errorf("invalid encrypted payload size: %d", encryptedSize)
	}

	return chunks, nil
}

func PlaintextSize(encryptedSize int64) (int64, error) {
	chunks, err := EncryptedChunkCount(encryptedSize)

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Verify the size you pass is the ciphertext (encrypted) size, not the plaintext size; ciphertext = plaintext + 16 bytes per 64 KiB chunk.
  2. Check the value is non-negative and comes from a reliable source such as os.File.Stat().Size() after the write completed.
  3. If the file may be truncated or incomplete, wait for the writer to finish or re-stat the file before computing sizes.
  4. If a sentinel negative value is in play, handle 'unknown size' explicitly before calling the API.

Example fix

// before
n, err := stream.EncryptedChunkCount(-1) // sentinel for unknown
// after
if size < 0 {
    return 0, errors.New("encrypted size unknown; stat the file first")
}
n, err := stream.EncryptedChunkCount(size)
Defensive patterns

Strategy: validation

Validate before calling

const encChunkSize = 64*1024 + 16
func validEncryptedSize(size int64) bool {
    return size >= 0 && size <= math.MaxInt64-encChunkSize+1
}
// call EncryptedChunkCount only if validEncryptedSize(size)

Try / catch

chunks, err := stream.EncryptedChunkCount(size)
if err != nil {
    return fmt.Errorf("cannot determine chunk layout for size %d: %w", size, err)
}

Prevention

When it happens

Trigger: Calling stream.EncryptedChunkCount(encryptedSize) with encryptedSize < 0 or with encryptedSize > math.MaxInt64 - encChunkSize + 1. Also surfaces via PlaintextSize and NewDecryptReaderAt, which call EncryptedChunkCount internally.

Common situations: Passing a plaintext file size instead of the ciphertext size; passing an os.Stat size from a truncated or still-being-written file; integer misuse where -1 is used as a sentinel for 'unknown size'.

Related errors


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