{"record":{"id":"c26b563e84edc980","repo":"JuliusBrussee/caveman","slug":"secretbox-production-refuses-legacy-local-ciphert","errorCode":null,"errorMessage":"secretbox: production refuses legacy local ciphertext","messagePattern":"secretbox: production refuses legacy local ciphertext","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/secretbox/secretbox.go","lineNumber":116,"sourceCode":"\t\treturn wrapped, nil\n\t}\n\treturn Encrypt(plaintext)\n}\n\n// Decrypt reverses Encrypt: it expects nonce(12) || ciphertext+tag.\nfunc Decrypt(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.Decrypt(ctx, envelope)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"secretbox: KMS decrypt: %w\", err)\n\t\t}\n\t\treturn plaintext, nil\n\t}\n\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:]","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/secretbox/secretbox.go#L98-L134","documentation":"secretbox.Decrypt only accepts legacy local AES-GCM ciphertext in non-production runtimes. In production (runtimeenv.IsProduction()) any non-KMS-envelope input is refused outright unless the operator explicitly sets CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT=true. This forces migration off local-key ciphertext before serving production traffic.","triggerScenarios":"Calling secretbox.Decrypt in a production runtime on bytes that are NOT a KMS envelope (e.g. old nonce(12)||ciphertext+tag blobs encrypted with the local key) while CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT is unset or not 'true'.","commonSituations":"Upgrading a deployment that stored pre-KMS ciphertext in its database; promoting a staging environment (which tolerated legacy blobs) to production; a backup restored into a prod database still containing old-format secrets.","solutions":["Preferred: run a one-off migration that decrypts legacy blobs and re-encrypts them with secretbox.Encrypt so they become KMS envelopes, then serve production without the escape hatch.","If you must read legacy data during migration, set CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT=true in the production environment and remove it as soon as migration completes.","Verify runtimeenv detection (e.g. env markers) is not accidentally classifying a dev box as production."],"exampleFix":"// before: prod boot fails/returns error on legacy rows\npt, err := secretbox.Decrypt(row.Secret)\n\n// after: one-off migration job\nif !kms.IsEnvelope(row.Secret) {\n    pt, err := secretbox.Decrypt(row.Secret) // run with env flag set, in a migration job\n    if err != nil { return err }\n    re, err := secretbox.Encrypt(pt) // produces KMS envelope\n    if err != nil { return err }\n    row.Secret = re\n    return db.Save(row).Error\n}","handlingStrategy":"validation","validationCode":"if runtimeenv.IsProduction() && !kms.IsEnvelope(blob) {\n    return fmt.Errorf(\"refusing legacy ciphertext in production; migrate to KMS envelope first\")\n}","typeGuard":null,"tryCatchPattern":"pt, err := secretbox.Decrypt(blob)\nif err != nil && strings.Contains(err.Error(), \"production refuses legacy local ciphertext\") {\n    // route to migration path instead of serving\n}","preventionTips":["Run the re-encryption migration in staging before promoting to production.","Set CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT only for the migration job's environment, never as a standing prod default.","Alert when non-envelope ciphertext is still present in production data."],"tags":["crypto","secretbox","kms","migration","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}