JuliusBrussee/caveman · error
ciphertext too short
Error message
ciphertext too short
What it means
secretbox.Decrypt rejects an encrypted envelope that is shorter than the AES-GCM nonce size (12 bytes for the standard block). Such input cannot possibly contain a nonce plus ciphertext, so decryption is refused up front rather than letting GCM.Open fail with a confusing low-level error. It indicates the caller passed truncated, empty, or non-secretbox bytes to Decrypt.
Solutions
- Verify the env var / byte slice holds the full secretbox envelope: re-encode it with secretbox.Encrypt and compare lengths (must be > 12 bytes).
- Check the transport/storage step (DB column size, config loader, HTTP body reader) for truncation of the base64 value.
- Confirm you are not passing a plaintext value or a KMS envelope into Decrypt directly; use DecryptPayloadKey which dispatches KMS envelopes.
- Re-encrypt the secret at the source with the current key and redeploy the ciphertext.
Example fix
// before
wrapped, _ := base64.StdEncoding.DecodeString(os.Getenv("APP_SECRET"))
key, err := secretbox.Decrypt(wrapped) // ciphertext too short
// after
encoded := strings.TrimSpace(os.Getenv("APP_SECRET"))
if encoded == "" {
return nil, fmt.Errorf("APP_SECRET is unset")
}
wrapped, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("APP_SECRET is not valid base64: %w", err)
}
if len(wrapped) <= 12 {
return nil, fmt.Errorf("APP_SECRET envelope truncated (%d bytes)", len(wrapped))
}
key, err := secretbox.Decrypt(wrapped) Defensive patterns
Strategy: validation
Validate before calling
if len(envelope) < 12 { // gcm nonce size
return fmt.Errorf("secret envelope too short (%d bytes); re-export the secret", len(envelope))
} Type guard
func looksLikeSecretboxEnvelope(b []byte) bool { return len(b) > 12 } Try / catch
plain, err := secretbox.Decrypt(envelope)
if err != nil {
if strings.Contains(err.Error(), "ciphertext too short") {
return fmt.Errorf("secret envelope truncated or empty; re-provision it: %w", err)
}
return err
} Prevention
- Never pass empty/nil byte slices into Decrypt; check length first.
- Use fixed-size columns / length-prefixed storage so ciphertext cannot be truncated.
- Round-trip test (Encrypt → Decrypt) in CI to catch transport truncation early.
When it happens
Trigger: Calling Decrypt (directly or via DecryptPayloadKey/ResolveEnvironmentSecret) with a zero-length slice, a slice shorter than 12 bytes, a byte slice that was truncated during transport, or a non-secretbox value such as a raw KMS envelope fragment or base64 of an empty/short string.
Common situations: Storing the ciphertext env var truncated by an ops tool that strips padding; copying only part of a base64 blob; a nil/empty value decoded from an unset variable; schema migrations that shortened a column.
Related errors
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/1fe530d43b22a78f.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/secretbox/secretbox.go:132
if runtimeenv.IsProduction() &&
!strings.EqualFold(strings.TrimSpace(os.Getenv("CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT")), "true") {
return nil, fmt.Errorf("secretbox: production refuses legacy local ciphertext")
}
keyBytes, err := loadKey()
if err != nil {
return nil, err
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
return nil, fmt.Errorf("aes cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("aes-gcm: %w", err)
}
ns := gcm.NonceSize()
if len(envelope) < ns {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ct := envelope[:ns], envelope[ns:]
plain, err := gcm.Open(nil, nonce, ct, nil)
if err != nil {
return nil, fmt.Errorf("aes-gcm open: %w", err)
}
return plain, nil
}
// DecryptPayloadKey unwraps an artifact data-encryption key. KMS envelopes are
// restricted to the configured payload key plus the explicit legacy secrets
// key used before key separation.
func DecryptPayloadKey(envelope []byte) ([]byte, error) {
if kms.IsEnvelope(envelope) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
plaintext, err := kms.DecryptPayload(ctx, envelope)
if err != nil {View on GitHub (pinned to 3ee70a1026)