{"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/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/secretbox/secretbox.go#L98-L134","documentation":"When Decrypt receives a non-KMS (legacy local AES-GCM) ciphertext in production, it refuses to decrypt it unless CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT is exactly 'true'. This guard prevents production services from silently relying on statically shared local keys, which undermines the KMS migration. It is an explicit migration-safety sentinel.","triggerScenarios":"Calling Decrypt (or DecryptPayloadKey/ResolveEnvironmentSecret) in production with an envelope that fails kms.IsEnvelope (i.e. legacy locally-encrypted data) while CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT is unset or not 'true'.","commonSituations":"Reading old database rows or config blobs encrypted before the KMS migration; restoring a backup containing legacy ciphertext into a KMS-enabled production environment; a service partially migrated where some producers still run with local keys.","solutions":["Re-encrypt legacy data with KMS: decrypt it once in a controlled non-production job or with the legacy flag, then store KMS envelopes.","As a short-term bridge, set CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT=true in production AND ensure CAVE_SECRETBOX_KEY holds the exact original local key — then migrate off it.","Identify which producer is still writing local ciphertext and move it to KMS encryption.","Audit data stores for legacy envelopes (they lack the KMS envelope header detected by kms.IsEnvelope) and schedule migration."],"exampleFix":"// before: prod reads legacy row -> secretbox: production refuses legacy local ciphertext\nplaintext, err := secretbox.Decrypt(legacyEnvelope)\n\n// after: one-time migration job (temporary opt-out + re-encrypt)\nos.Setenv(\"CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT\", \"true\")\npt, _ := secretbox.Decrypt(legacyEnvelope)\nnewEnv, _ := secretbox.Encrypt(pt) // KMS envelope now that provider is set","handlingStrategy":"validation","validationCode":"func canDecryptLegacyInProduction() error {\n    if runtimeenv.IsProduction() &&\n        !strings.EqualFold(strings.TrimSpace(os.Getenv(\"CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT\")), \"true\") {\n        // caller should re-encrypt data instead of relying on legacy local keys\n        return errors.New(\"legacy local ciphertext present; migrate to KMS envelopes or set temporary opt-in\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"pt, err := secretbox.Decrypt(envelope)\nif err != nil {\n    if strings.Contains(err.Error(), \"refuses legacy local ciphertext\") {\n        // route to migration job: decrypt with opt-in once, re-encrypt via KMS\n        return migrateToKMSEnvelope(envelope)\n    }\n    return err\n}","preventionTips":["Plan and execute a full re-encryption migration before enabling KMS-only production mode.","Detect legacy envelopes up front (kms.IsEnvelope == false) and batch-migrate them.","Treat CAVE_KMS_ALLOW_LEGACY_LOCAL_DECRYPT=true as a temporary, audited bridge with an expiry.","Ensure no producer can still write local ciphertext once the migration is declared complete."],"tags":["decryption","migration","kms","production","go"],"backgroundTag":"unsupported-operation","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"}