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...), nilView on GitHub (pinned to 27d5a3981a)
Solutions
- Switch to envelope encryption: generate a 32-byte data key, encrypt payload with AES-GCM locally, and Encrypt only the data key
- If the payload must be single-blob, chunk it under 65535 bytes and track chunk order yourself
- Add a length check in the calling code so oversized input fails with your own clearer error
- 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
- Use envelope encryption (encrypt a 32-byte DEK, AES-GCM the payload)
- Assert len(plaintext) <= 65535 at the API boundary that accepts secrets
- Never pass unbounded user input straight to Encrypt
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
- cave_memory_too_large
- ErrMemoryTooLarge
- production KMS configuration: %w
- sanitize row %d: events exceed %d bytes
- kms: unsupported provider %q
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/11bc25c5e3a6c6c8.
Report an issue: GitHub.