{"record":{"id":"1fe530d43b22a78f","repo":"JuliusBrussee/caveman","slug":"ciphertext-too-short","errorCode":null,"errorMessage":"ciphertext too short","messagePattern":"ciphertext too short","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/secretbox/secretbox.go","lineNumber":132,"sourceCode":"\tif runtimeenv.IsProduction() &&\n\t\t!strings.EqualFold(strings.TrimSpace(os.Getenv(\"CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT\")), \"true\") {\n\t\treturn nil, fmt.Errorf(\"secretbox: production refuses legacy local ciphertext\")\n\t}\n\tkeyBytes, err := loadKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblock, err := aes.NewCipher(keyBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aes cipher: %w\", err)\n\t}\n\tgcm, err := cipher.NewGCM(block)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aes-gcm: %w\", err)\n\t}\n\tns := gcm.NonceSize()\n\tif len(envelope) < ns {\n\t\treturn nil, fmt.Errorf(\"ciphertext too short\")\n\t}\n\tnonce, ct := envelope[:ns], envelope[ns:]\n\tplain, err := gcm.Open(nil, nonce, ct, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aes-gcm open: %w\", err)\n\t}\n\treturn plain, nil\n}\n\n// DecryptPayloadKey unwraps an artifact data-encryption key. KMS envelopes are\n// restricted to the configured payload key plus the explicit legacy secrets\n// key used before key separation.\nfunc DecryptPayloadKey(envelope []byte) ([]byte, error) {\n\tif kms.IsEnvelope(envelope) {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\t\tplaintext, err := kms.DecryptPayload(ctx, envelope)\n\t\tif err != nil {","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/secretbox/secretbox.go#L114-L150","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nwrapped, _ := base64.StdEncoding.DecodeString(os.Getenv(\"APP_SECRET\"))\nkey, err := secretbox.Decrypt(wrapped) // ciphertext too short\n\n// after\nencoded := strings.TrimSpace(os.Getenv(\"APP_SECRET\"))\nif encoded == \"\" {\n    return nil, fmt.Errorf(\"APP_SECRET is unset\")\n}\nwrapped, err := base64.StdEncoding.DecodeString(encoded)\nif err != nil {\n    return nil, fmt.Errorf(\"APP_SECRET is not valid base64: %w\", err)\n}\nif len(wrapped) <= 12 {\n    return nil, fmt.Errorf(\"APP_SECRET envelope truncated (%d bytes)\", len(wrapped))\n}\nkey, err := secretbox.Decrypt(wrapped)","handlingStrategy":"validation","validationCode":"if len(envelope) < 12 { // gcm nonce size\n    return fmt.Errorf(\"secret envelope too short (%d bytes); re-export the secret\", len(envelope))\n}","typeGuard":"func looksLikeSecretboxEnvelope(b []byte) bool { return len(b) > 12 }","tryCatchPattern":"plain, err := secretbox.Decrypt(envelope)\nif err != nil {\n    if strings.Contains(err.Error(), \"ciphertext too short\") {\n        return fmt.Errorf(\"secret envelope truncated or empty; re-provision it: %w\", err)\n    }\n    return err\n}","preventionTips":["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."],"tags":["go","crypto","aead","input-validation"],"backgroundTag":"ciphertext-too-short","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}