JuliusBrussee/caveman · error

kms: plaintext exceeds %d bytes

Error message

kms: plaintext exceeds %d bytes

What it means

Client.Encrypt rejects plaintexts larger than maxPlaintextBytes (65535 bytes, 64 KiB). This is a client-side pre-check before the Scaleway key-manager call, bounding envelope size and request body. Empty plaintext is rejected separately (error 'kms: plaintext is empty'), so this error is purely about the upper bound.

Source

Thrown at shared/platform/kms/kms.go:180

	return client.Encrypt(ctx, plaintext)
}

// EncryptPayload wraps an artifact data key with the dedicated payload KEK.
func EncryptPayload(ctx context.Context, plaintext []byte) ([]byte, error) {
	client, err := FromPayloadEnvironment()
	if err != nil {
		return nil, err
	}
	return client.Encrypt(ctx, plaintext)
}

// Encrypt delegates encryption to key manager.
func (c *Client) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error) {
	if len(plaintext) == 0 {
		return nil, errors.New("kms: plaintext is empty")
	}
	if len(plaintext) > maxPlaintextBytes {
		return nil, fmt.Errorf("kms: plaintext exceeds %d bytes", maxPlaintextBytes)
	}
	var response struct {
		KeyID      string `json:"key_id"`
		Ciphertext string `json:"ciphertext"`
	}
	if err := c.call(ctx, c.region, c.keyID, "encrypt", map[string]string{
		"plaintext": base64.StdEncoding.EncodeToString(plaintext),
	}, &response); err != nil {
		return nil, err
	}
	if response.KeyID != c.keyID || strings.TrimSpace(response.Ciphertext) == "" {
		return nil, errors.New("kms: invalid encrypt response")
	}
	envelope, err := json.Marshal(Envelope{Provider: c.provider, Region: c.region, KeyID: response.KeyID, Ciphertext: response.Ciphertext})
	if err != nil {
		return nil, fmt.Errorf("kms: encode envelope: %w", err)
	}
	return append([]byte(prefix), envelope...), nil

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Switch to envelope encryption: generate a 32-byte data key, encrypt payload with AES-GCM locally, and Encrypt only the data key
  2. If the payload must be single-blob, chunk it under 65535 bytes and track chunk order yourself
  3. Add a length check in the calling code so oversized input fails with your own clearer error
  4. For secrets like tokens and PEM keys (well under 64 KiB), no change is needed

Example fix

// before
ciphertext, err := client.Encrypt(ctx, largeSecret) // 500KB

// after
dek := make([]byte, 32)
rand.Read(dek)
nonce := make([]byte, 12)
rand.Read(nonce)
block, _ := aes.NewCipher(dek)
gcm, _ := cipher.NewGCM(block)
body := gcm.Seal(nil, nonce, largeSecret, nil)
wrappedDEK, err := client.Encrypt(ctx, dek)
_ = body; _ = nonce; _ = wrappedDEK // persist together
Defensive patterns

Strategy: validation

Validate before calling

const maxPT = 65535
func plaintextWithinLimit(b []byte) bool { return len(b) > 0 && len(b) <= maxPT }

Try / catch

if _, err := client.Encrypt(ctx, secret); err != nil { if strings.Contains(err.Error(), "plaintext exceeds") { return encryptEnvelope(secret) /* DEK pattern */ } }

Prevention

When it happens

Trigger: Encrypting a document, image, or large config blob directly instead of a data key; passing an unbounded io.ReadAll result into Encrypt; encrypting a full API response body of hundreds of KB.

Common situations: Using envelope encryption incorrectly — encrypting the payload itself rather than a symmetric key; log/backup pipelines that grew past 64 KiB per record over time; merging multiple secrets into one blob before encryption.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/11bc25c5e3a6c6c8. Report an issue: GitHub.