This is an automated email from the ASF dual-hosted git repository.

baoyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 9a2380c68 feat: enhance encrypt_fields to support nested structures 
(#13192)
9a2380c68 is described below

commit 9a2380c6826e87e8b7080f2f6a8e6b0b1c39c8fe
Author: Nic <[email protected]>
AuthorDate: Fri Apr 10 10:38:47 2026 +0800

    feat: enhance encrypt_fields to support nested structures (#13192)
---
 apisix/plugin.lua                  | 154 ++++++-----
 apisix/plugins/ai-proxy/schema.lua |  10 +-
 apisix/plugins/ai-rag.lua          |   6 +-
 t/node/data_encrypt3.t             | 537 +++++++++++++++++++++++++++++++++++++
 4 files changed, 641 insertions(+), 66 deletions(-)

diff --git a/apisix/plugin.lua b/apisix/plugin.lua
index 8b4177ae0..723ededd0 100644
--- a/apisix/plugin.lua
+++ b/apisix/plugin.lua
@@ -21,7 +21,7 @@ local enable_debug  = require("apisix.debug").enable_debug
 local wasm          = require("apisix.wasm")
 local expr          = require("resty.expr.v1")
 local apisix_ssl    = require("apisix.ssl")
-local re_split      = require("ngx.re").split
+
 local ngx           = ngx
 local ngx_ok        = ngx.OK
 local ngx_print     = ngx.print
@@ -988,6 +988,88 @@ local function get_plugin_schema_for_gde(name, schema_type)
 end
 
 
+-- Process a single encrypt_field path on the given config table.
+-- Supports:
+--   - Arbitrary depth dotted paths (e.g., "a.b.c.d")
+--   - Array traversal at intermediate nodes (iterate each element)
+--   - Leaf type dispatch: string, array of strings, map of strings
+local function process_encrypt_field(conf, key_path, operation, plugin_name, 
op_name)
+    local dot_pos = core.string.find(key_path, ".")
+
+    if not dot_pos then
+        -- leaf segment
+        local val = conf[key_path]
+        if val == nil then
+            return
+        end
+
+        if type(val) == "string" then
+            local result, err = operation(val, "data_encrypt")
+            if not result then
+                core.log.warn("failed to ", op_name, " the conf of plugin [",
+                              plugin_name, "] key [", key_path, "], err: ", 
err)
+            else
+                conf[key_path] = result
+            end
+
+        elseif type(val) == "table" then
+            if core.table.isarray(val) then
+                -- array of strings
+                for i, item in ipairs(val) do
+                    if type(item) == "string" then
+                        local result, err = operation(item, "data_encrypt")
+                        if not result then
+                            core.log.warn("failed to ", op_name, " the conf of 
plugin [",
+                                          plugin_name, "] key [", key_path,
+                                          "] index [", i, "], err: ", err)
+                        else
+                            val[i] = result
+                        end
+                    end
+                end
+            else
+                -- map of strings
+                for k, v in pairs(val) do
+                    if type(v) == "string" then
+                        local result, err = operation(v, "data_encrypt")
+                        if not result then
+                            core.log.warn("failed to ", op_name, " the conf of 
plugin [",
+                                          plugin_name, "] key [", key_path,
+                                          ".", k, "], err: ", err)
+                        else
+                            val[k] = result
+                        end
+                    end
+                end
+            end
+        end
+
+    else
+        -- intermediate segment: split on first dot and recurse
+        local segment = key_path:sub(1, dot_pos - 1)
+        local rest = key_path:sub(dot_pos + 1)
+        local val = conf[segment]
+
+        if val == nil or type(val) ~= "table" then
+            return
+        end
+
+        if core.table.isarray(val) then
+            -- array: iterate each element and recurse
+            for _, item in ipairs(val) do
+                if type(item) == "table" then
+                    process_encrypt_field(item, rest, operation, plugin_name, 
op_name)
+                end
+            end
+        else
+            -- map: recurse into it
+            process_encrypt_field(val, rest, operation, plugin_name, op_name)
+        end
+    end
+end
+_M.process_encrypt_field = process_encrypt_field
+
+
 local function decrypt_conf(name, conf, schema_type)
     if not enable_gde() then
         return
@@ -1000,34 +1082,7 @@ local function decrypt_conf(name, conf, schema_type)
 
     if schema.encrypt_fields and not core.table.isempty(schema.encrypt_fields) 
then
         for _, key in ipairs(schema.encrypt_fields) do
-            if conf[key] then
-                local decrypted, err = apisix_ssl.aes_decrypt_pkey(conf[key], 
"data_encrypt")
-                if not decrypted then
-                    core.log.warn("failed to decrypt the conf of plugin [", 
name,
-                                  "] key [", key, "], err: ", err)
-                else
-                    conf[key] = decrypted
-                end
-            elseif core.string.find(key, ".") then
-                -- decrypt fields has indents
-                local res, err = re_split(key, "\\.", "jo")
-                if not res then
-                    core.log.warn("failed to split key [", key, "], err: ", 
err)
-                    return
-                end
-
-                -- we only support two levels
-                if conf[res[1]] and conf[res[1]][res[2]] then
-                    local decrypted, err = apisix_ssl.aes_decrypt_pkey(
-                                           conf[res[1]][res[2]], 
"data_encrypt")
-                    if not decrypted then
-                        core.log.warn("failed to decrypt the conf of plugin 
[", name,
-                                      "] key [", key, "], err: ", err)
-                    else
-                        conf[res[1]][res[2]] = decrypted
-                    end
-                end
-            end
+            process_encrypt_field(conf, key, apisix_ssl.aes_decrypt_pkey, 
name, "decrypt")
         end
     end
 end
@@ -1046,34 +1101,7 @@ local function encrypt_conf(name, conf, schema_type)
 
     if schema.encrypt_fields and not core.table.isempty(schema.encrypt_fields) 
then
         for _, key in ipairs(schema.encrypt_fields) do
-            if conf[key] then
-                local encrypted, err = apisix_ssl.aes_encrypt_pkey(conf[key], 
"data_encrypt")
-                if not encrypted then
-                    core.log.warn("failed to encrypt the conf of plugin [", 
name,
-                                  "] key [", key, "], err: ", err)
-                else
-                    conf[key] = encrypted
-                end
-            elseif core.string.find(key, ".") then
-                -- encrypt fields has indents
-                local res, err = re_split(key, "\\.", "jo")
-                if not res then
-                    core.log.warn("failed to split key [", key, "], err: ", 
err)
-                    return
-                end
-
-                -- we only support two levels
-                if conf[res[1]] and conf[res[1]][res[2]] then
-                    local encrypted, err = apisix_ssl.aes_encrypt_pkey(
-                                           conf[res[1]][res[2]], 
"data_encrypt")
-                    if not encrypted then
-                        core.log.warn("failed to encrypt the conf of plugin 
[", name,
-                                      "] key [", key, "], err: ", err)
-                    else
-                        conf[res[1]][res[2]] = encrypted
-                    end
-                end
-            end
+            process_encrypt_field(conf, key, apisix_ssl.aes_encrypt_pkey, 
name, "encrypt")
         end
     end
 end
@@ -1142,16 +1170,16 @@ _M.stream_check_schema = stream_check_schema
 
 function _M.plugin_checker(item, schema_type)
     if item.plugins then
-        local skip_disabled_plugins = not (core.config.type == "yaml" or 
core.config.type == "json")
-        local ok, err = check_schema(item.plugins, schema_type, 
skip_disabled_plugins)
-
-        if ok and enable_gde() then
-            -- decrypt conf
+        if enable_gde() then
+            -- decrypt conf before validation so that content-level checks
+            -- (e.g. ai-proxy service_account_json JSON parsing) see plaintext
             for name, conf in pairs(item.plugins) do
                 decrypt_conf(name, conf, schema_type)
             end
         end
-        return ok, err
+
+        local skip_disabled_plugins = not (core.config.type == "yaml" or 
core.config.type == "json")
+        return check_schema(item.plugins, schema_type, skip_disabled_plugins)
     end
 
     return true
diff --git a/apisix/plugins/ai-proxy/schema.lua 
b/apisix/plugins/ai-proxy/schema.lua
index 4fef8ee01..397f210f3 100644
--- a/apisix/plugins/ai-proxy/schema.lua
+++ b/apisix/plugins/ai-proxy/schema.lua
@@ -202,7 +202,8 @@ _M.ai_proxy_schema = {
             },
         },
     },
-    required = {"provider", "auth"}
+    required = {"provider", "auth"},
+    encrypt_fields = {"auth.header", "auth.query", 
"auth.gcp.service_account_json"},
 }
 
 _M.ai_proxy_multi_schema = {
@@ -267,7 +268,12 @@ _M.ai_proxy_multi_schema = {
         keepalive_pool = {type = "integer", minimum = 1, default = 30},
         ssl_verify = {type = "boolean", default = true },
     },
-    required = {"instances"}
+    required = {"instances"},
+    encrypt_fields = {
+        "instances.auth.header",
+        "instances.auth.query",
+        "instances.auth.gcp.service_account_json",
+    },
 }
 
 return  _M
diff --git a/apisix/plugins/ai-rag.lua b/apisix/plugins/ai-rag.lua
index d897fb528..fa40f5605 100644
--- a/apisix/plugins/ai-rag.lua
+++ b/apisix/plugins/ai-rag.lua
@@ -53,7 +53,11 @@ local schema = {
             maxProperties = 1
         },
     },
-    required = { "embeddings_provider", "vector_search_provider" }
+    required = { "embeddings_provider", "vector_search_provider" },
+    encrypt_fields = {
+        "embeddings_provider.azure_openai.api_key",
+        "vector_search_provider.azure_ai_search.api_key",
+    },
 }
 
 local request_schema = {
diff --git a/t/node/data_encrypt3.t b/t/node/data_encrypt3.t
new file mode 100644
index 000000000..407c276a6
--- /dev/null
+++ b/t/node/data_encrypt3.t
@@ -0,0 +1,537 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+log_level("info");
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!defined $block->request) {
+        $block->set_value("request", "GET /t");
+    }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: ai-proxy: encrypt auth.header (map of strings) and 
auth.gcp.service_account_json (3-level nested string)
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": {
+                                "header": {
+                                    "Authorization": "Bearer sk-test-key"
+                                },
+                                "query": {
+                                    "api-key": "my-query-secret"
+                                },
+                                "gcp": {
+                                    "service_account_json": 
"{\"type\":\"service_account\"}"
+                                }
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.sleep(0.1)
+
+            -- admin API should return decrypted values
+            local code, message, res = t('/apisix/admin/routes/1',
+                ngx.HTTP_GET
+            )
+            res = json.decode(res)
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+
+            local ai_proxy = res.value.plugins["ai-proxy"]
+            ngx.say("header.Authorization: ", 
ai_proxy.auth.header.Authorization)
+            ngx.say("query.api-key: ", ai_proxy.auth.query["api-key"])
+            ngx.say("gcp.service_account_json: ", 
ai_proxy.auth.gcp.service_account_json)
+
+            -- etcd should have encrypted values
+            local etcd = require("apisix.core.etcd")
+            local res = assert(etcd.get('/routes/1'))
+            local ai_proxy_etcd = res.body.node.value.plugins["ai-proxy"]
+            ngx.say("etcd header encrypted: ",
+                    ai_proxy_etcd.auth.header.Authorization ~= "Bearer 
sk-test-key")
+            ngx.say("etcd query encrypted: ",
+                    ai_proxy_etcd.auth.query["api-key"] ~= "my-query-secret")
+            ngx.say("etcd gcp encrypted: ",
+                    ai_proxy_etcd.auth.gcp.service_account_json ~= 
"{\"type\":\"service_account\"}")
+        }
+    }
+--- response_body
+header.Authorization: Bearer sk-test-key
+query.api-key: my-query-secret
+gcp.service_account_json: {"type":"service_account"}
+etcd header encrypted: true
+etcd query encrypted: true
+etcd gcp encrypted: true
+
+
+
+=== TEST 2: ai-proxy-multi: encrypt instances[].auth.header (array with nested 
map of strings)
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "ai-proxy-multi": {
+                            "instances": [
+                                {
+                                    "name": "openai-1",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer 
sk-instance1-key"
+                                        }
+                                    }
+                                },
+                                {
+                                    "name": "openai-2",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer 
sk-instance2-key"
+                                        },
+                                        "gcp": {
+                                            "service_account_json": 
"{\"type\":\"service_account\",\"project_id\":\"test\"}"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.sleep(0.1)
+
+            -- admin API should return decrypted values
+            local code, message, res = t('/apisix/admin/routes/1',
+                ngx.HTTP_GET
+            )
+            res = json.decode(res)
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+
+            local multi = res.value.plugins["ai-proxy-multi"]
+            ngx.say("instance1 header: ", 
multi.instances[1].auth.header.Authorization)
+            ngx.say("instance2 header: ", 
multi.instances[2].auth.header.Authorization)
+            ngx.say("instance2 gcp: ", 
multi.instances[2].auth.gcp.service_account_json)
+
+            -- etcd should have encrypted values
+            local etcd = require("apisix.core.etcd")
+            local res = assert(etcd.get('/routes/1'))
+            local multi_etcd = res.body.node.value.plugins["ai-proxy-multi"]
+            ngx.say("etcd instance1 header encrypted: ",
+                    multi_etcd.instances[1].auth.header.Authorization ~= 
"Bearer sk-instance1-key")
+            ngx.say("etcd instance2 header encrypted: ",
+                    multi_etcd.instances[2].auth.header.Authorization ~= 
"Bearer sk-instance2-key")
+            ngx.say("etcd instance2 gcp encrypted: ",
+                    multi_etcd.instances[2].auth.gcp.service_account_json ~=
+                    "{\"type\":\"service_account\",\"project_id\":\"test\"}")
+        }
+    }
+--- response_body
+instance1 header: Bearer sk-instance1-key
+instance2 header: Bearer sk-instance2-key
+instance2 gcp: {"type":"service_account","project_id":"test"}
+etcd instance1 header encrypted: true
+etcd instance2 header encrypted: true
+etcd instance2 gcp encrypted: true
+
+
+
+=== TEST 3: ai-rag: encrypt deeply nested api_key fields (3-level dotted path)
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "ai-rag": {
+                            "embeddings_provider": {
+                                "azure_openai": {
+                                    "endpoint": 
"https://test.openai.azure.com/embeddings";,
+                                    "api_key": "embeddings-secret-key"
+                                }
+                            },
+                            "vector_search_provider": {
+                                "azure_ai_search": {
+                                    "endpoint": 
"https://test.search.windows.net/indexes/idx/docs/search";,
+                                    "api_key": "search-secret-key"
+                                }
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.sleep(0.1)
+
+            -- admin API should return decrypted values
+            local code, message, res = t('/apisix/admin/routes/1',
+                ngx.HTTP_GET
+            )
+            res = json.decode(res)
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+
+            local ai_rag = res.value.plugins["ai-rag"]
+            ngx.say("embeddings api_key: ",
+                    ai_rag.embeddings_provider.azure_openai.api_key)
+            ngx.say("search api_key: ",
+                    ai_rag.vector_search_provider.azure_ai_search.api_key)
+
+            -- etcd should have encrypted values
+            local etcd = require("apisix.core.etcd")
+            local res = assert(etcd.get('/routes/1'))
+            local ai_rag_etcd = res.body.node.value.plugins["ai-rag"]
+            ngx.say("etcd embeddings encrypted: ",
+                    ai_rag_etcd.embeddings_provider.azure_openai.api_key ~= 
"embeddings-secret-key")
+            ngx.say("etcd search encrypted: ",
+                    ai_rag_etcd.vector_search_provider.azure_ai_search.api_key 
~= "search-secret-key")
+        }
+    }
+--- response_body
+embeddings api_key: embeddings-secret-key
+search api_key: search-secret-key
+etcd embeddings encrypted: true
+etcd search encrypted: true
+
+
+
+=== TEST 4: process_encrypt_field handles nil and missing fields gracefully
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin").test
+            -- ai-proxy with no auth.gcp set: should not error
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": {
+                                "header": {
+                                    "Authorization": "Bearer sk-only-header"
+                                }
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.sleep(0.1)
+
+            local code, message, res = t('/apisix/admin/routes/1',
+                ngx.HTTP_GET
+            )
+            res = json.decode(res)
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+
+            local ai_proxy = res.value.plugins["ai-proxy"]
+            ngx.say("header.Authorization: ", 
ai_proxy.auth.header.Authorization)
+            ngx.say("query is nil: ", ai_proxy.auth.query == nil)
+            ngx.say("gcp is nil: ", ai_proxy.auth.gcp == nil)
+        }
+    }
+--- response_body
+header.Authorization: Bearer sk-only-header
+query is nil: true
+gcp is nil: true
+
+
+
+=== TEST 5: regression: flat key encryption still works (basic-auth password)
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "test_encrypt3",
+                    "plugins": {
+                        "basic-auth": {
+                            "username": "foo",
+                            "password": "bar"
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.sleep(0.1)
+
+            -- admin API returns decrypted
+            local code, message, res = 
t('/apisix/admin/consumers/test_encrypt3',
+                ngx.HTTP_GET
+            )
+            res = json.decode(res)
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+            ngx.say(res.value.plugins["basic-auth"].password)
+
+            -- etcd stores encrypted
+            local etcd = require("apisix.core.etcd")
+            local res = assert(etcd.get('/consumers/test_encrypt3'))
+            ngx.say(res.body.node.value.plugins["basic-auth"].password ~= 
"bar")
+        }
+    }
+--- response_body
+bar
+true
+
+
+
+=== TEST 6: regression: 2-level dotted path encryption still works 
(kafka-proxy sasl.password)
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "kafka-proxy": {
+                            "sasl": {
+                                "username": "admin",
+                                "password": "admin-secret"
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.sleep(0.1)
+
+            local code, message, res = t('/apisix/admin/routes/1',
+                ngx.HTTP_GET
+            )
+            res = json.decode(res)
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(message)
+                return
+            end
+            ngx.say(res.value.plugins["kafka-proxy"].sasl.password)
+
+            local etcd = require("apisix.core.etcd")
+            local res = assert(etcd.get('/routes/1'))
+            ngx.say(res.body.node.value.plugins["kafka-proxy"].sasl.password 
~= "admin-secret")
+        }
+    }
+--- response_body
+admin-secret
+true
+
+
+
+=== TEST 7: encrypt_fields with array of strings leaf via process_encrypt_field
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugin")
+            local ssl = require("apisix.ssl")
+
+            -- Simulate array-of-strings encryption (e.g., secret_fallbacks)
+            local conf = {
+                secrets = {"secret-one", "secret-two", "secret-three"}
+            }
+
+            -- Encrypt
+            plugin.process_encrypt_field(conf, "secrets", 
ssl.aes_encrypt_pkey, "test", "encrypt")
+
+            -- Verify all elements are encrypted (not plaintext)
+            for i, v in ipairs(conf.secrets) do
+                ngx.say("encrypted[" .. i .. "] differs: ", v ~= "secret-" ..
+                    (i == 1 and "one" or i == 2 and "two" or "three"))
+            end
+
+            -- Decrypt
+            plugin.process_encrypt_field(conf, "secrets", 
ssl.aes_decrypt_pkey, "test", "decrypt")
+
+            -- Verify all elements are restored
+            ngx.say("decrypted[1]: ", conf.secrets[1])
+            ngx.say("decrypted[2]: ", conf.secrets[2])
+            ngx.say("decrypted[3]: ", conf.secrets[3])
+        }
+    }
+--- response_body
+encrypted[1] differs: true
+encrypted[2] differs: true
+encrypted[3] differs: true
+decrypted[1]: secret-one
+decrypted[2]: secret-two
+decrypted[3]: secret-three

Reply via email to