{"record":{"id":"0e59cfe71dd91403","repo":"JuliusBrussee/caveman","slug":"s-must-decode-to-exactly-32-bytes-got-d","errorCode":null,"errorMessage":"%s must decode to exactly 32 bytes, got %d","messagePattern":"(.+?) must decode to exactly 32 bytes, got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/secretbox/secretbox.go","lineNumber":46,"sourceCode":"\t\"github.com/JuliusBrussee/caveman/shared/platform/runtimeenv\"\n)\n\n// envKey is the name of the environment variable holding the base64-encoded\n// 32-byte master key.\nconst envKey = \"CAVE_LOCAL_ENCRYPTION_KEY\"\n\n// loadKey reads and validates the 32-byte AES key from the environment.\nfunc loadKey() ([]byte, error) {\n\tkeyB64 := os.Getenv(envKey)\n\tif keyB64 == \"\" {\n\t\treturn nil, fmt.Errorf(\"%s is not set; cannot encrypt/decrypt secrets\", envKey)\n\t}\n\tkeyBytes, err := base64.StdEncoding.DecodeString(keyB64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s is not valid base64: %w\", envKey, err)\n\t}\n\tif len(keyBytes) != 32 {\n\t\treturn nil, fmt.Errorf(\"%s must decode to exactly 32 bytes, got %d\", envKey, len(keyBytes))\n\t}\n\treturn keyBytes, nil\n}\n\n// Encrypt seals plaintext with AES-256-GCM and a fresh random nonce, returning\n// nonce(12) || ciphertext+tag as raw bytes.\nfunc Encrypt(plaintext []byte) ([]byte, error) {\n\tif useKMS() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\t\twrapped, err := kms.Encrypt(ctx, plaintext)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"secretbox: KMS encrypt: %w\", err)\n\t\t}\n\t\treturn wrapped, nil\n\t}\n\tif runtimeenv.IsProduction() {\n\t\treturn nil, fmt.Errorf(\"secretbox: production requires CAVE_KMS_PROVIDER=scaleway\")","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/secretbox/secretbox.go#L28-L64","documentation":"The env variable decoded from base64 successfully but did not yield exactly 32 bytes — AES-256 requires a 256-bit key, and secretbox refuses to pad or derive, since a silently weakened or repeated-key AES-GCM would be a security defect. The message reports the actual decoded length (common wrong sizes: 16 from re-using an AES-128 key, 24, or 33+ from double-encoding).","triggerScenarios":"Local-mode Encrypt/Decrypt with a key generated for a different algorithm (AES-128: 16 bytes), a raw 36-byte UUID, a 64-byte random value, or a base64 string that was itself base64-encoded once more (48 bytes decoding to a 64-byte base64 payload).","commonSituations":"Reusing a pre-existing encryption key from another service with a different key size; generating with `head -c 16 /dev/urandom | base64`; misunderstanding that the value must encode exactly 32 raw bytes.","solutions":["Generate exactly 32 bytes and base64-encode them: openssl rand -base64 32 (output decodes to 32 bytes).","Verify before deploy: echo -n \"$CAVE_LOCAL_ENCRYPTION_KEY\" | base64 -d | wc -c must print 32.","If migrating from a non-32-byte key, re-encrypt stored secrets under the new key rather than trying to make the old key fit."],"exampleFix":"# before\nexport CAVE_LOCAL_ENCRYPTION_KEY=\"$(head -c 16 /dev/urandom | base64)\" # decodes to 16 bytes -> rejected\n\n# after\nexport CAVE_LOCAL_ENCRYPTION_KEY=\"$(openssl rand -base64 32)\"          # decodes to exactly 32 bytes","handlingStrategy":"validation","validationCode":"func localKeySizeOK() bool {\n    raw, err := base64.StdEncoding.DecodeString(os.Getenv(\"CAVE_LOCAL_ENCRYPTION_KEY\"))\n    return err == nil && len(raw) == 32\n}","typeGuard":null,"tryCatchPattern":"if _, err := secretbox.Encrypt(pt); err != nil {\n    if strings.Contains(err.Error(), \"exactly 32 bytes\") {\n        // replace the key with a 32-byte one and re-encrypt stored data under it\n    }\n}","preventionTips":["Shell check before deploy: echo -n \"$CAVE_LOCAL_ENCRYPTION_KEY\" | base64 -d | wc -c # must be 32.","Never pad/derive a wrong-size key to make it fit — regenerate and re-encrypt.","Document the key format (base64 of exactly 32 raw bytes) wherever the variable is introduced."],"tags":["secrets","crypto","aes-256","environment"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}