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

nic-6443 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 9a335dd08 refactor: decouple generic data encryption from ssl.lua 
(#13564)
9a335dd08 is described below

commit 9a335dd0827e8de2a5df044cdd0224ddf3e9e126
Author: Nic <[email protected]>
AuthorDate: Wed Jun 17 13:57:36 2026 +0800

    refactor: decouple generic data encryption from ssl.lua (#13564)
---
 apisix/core.lua                 |   1 +
 apisix/core/data_encryption.lua | 151 ++++++++++++++++++++++++++++++++++++++++
 apisix/plugin.lua               |  11 ++-
 apisix/ssl.lua                  | 104 +++------------------------
 t/node/data_encrypt.t           |   4 +-
 t/node/data_encrypt3.t          |  33 ++++++++-
 6 files changed, 198 insertions(+), 106 deletions(-)

diff --git a/apisix/core.lua b/apisix/core.lua
index ffae65f45..fceb7d6a0 100644
--- a/apisix/core.lua
+++ b/apisix/core.lua
@@ -65,4 +65,5 @@ return {
     math        = require("apisix.core.math"),
     event       = require("apisix.core.event"),
     env         = require("apisix.core.env"),
+    data_encryption = require("apisix.core.data_encryption"),
 }
diff --git a/apisix/core/data_encryption.lua b/apisix/core/data_encryption.lua
new file mode 100644
index 000000000..88bbaf107
--- /dev/null
+++ b/apisix/core/data_encryption.lua
@@ -0,0 +1,151 @@
+--
+-- 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.
+
+--- Generic AES-CBC data encryption used by the `data_encryption.keyring`
+-- (plugin `encrypt_fields`). The primitives are keyring-agnostic so other
+-- callers (e.g. SSL key encryption) can reuse them with their own keyring.
+--
+-- @module core.data_encryption
+
+local log               = require("apisix.core.log")
+local tbl               = require("apisix.core.table")
+local fetch_local_conf  = require("apisix.core.config_local").local_conf
+local aes               = require("resty.aes")
+local ffi               = require("ffi")
+
+local C                 = ffi.C
+local ngx_encode_base64 = ngx.encode_base64
+local ngx_decode_base64 = ngx.decode_base64
+local type              = type
+local ipairs            = ipairs
+local assert            = assert
+
+ffi.cdef[[
+unsigned long ERR_peek_error(void);
+void ERR_clear_error(void);
+]]
+
+
+local _M = {}
+
+
+--- Build a table of AES-128-CBC ciphers from a keyring, each using the key
+-- itself as the IV.
+function _M.init_iv_tbl(ivs)
+    local iv_tbl = tbl.new(2, 0)
+    if type(ivs) == "string" then
+        ivs = {ivs}
+    end
+
+    if type(ivs) == "table" then
+        for _, iv in ipairs(ivs) do
+            tbl.insert(iv_tbl, assert(aes:new(iv, nil, aes.cipher(128, "cbc"), 
{iv = iv})))
+        end
+    end
+
+    return iv_tbl
+end
+
+
+--- Encrypt `value` with the first cipher of `iv_tbl` and base64-encode it.
+function _M.aes_cbc_encrypt(iv_tbl, value)
+    local aes_cbc_with_iv = iv_tbl[1]
+    if aes_cbc_with_iv == nil then
+        return nil, "no keyring configured"
+    end
+
+    local encrypted = aes_cbc_with_iv:encrypt(value)
+    if encrypted == nil then
+        return nil, "failed to encrypt"
+    end
+
+    return ngx_encode_base64(encrypted)
+end
+
+
+--- Base64-decode `value`, then try to decrypt it with each cipher of `iv_tbl`.
+-- `subject` (optional) is a noun describing what is being decrypted; it is 
woven
+-- into the error message so callers get a meaningful reason (e.g. "ssl key" ->
+-- "base64 decode ssl key failed"). Generic messages are used when omitted.
+function _M.aes_cbc_decrypt(iv_tbl, value, subject)
+    local what = subject and (subject .. " ") or ""
+
+    local decoded = ngx_decode_base64(value)
+    if not decoded then
+        return nil, "base64 decode " .. what .. "failed"
+    end
+
+    for _, aes_cbc_with_iv in ipairs(iv_tbl) do
+        local decrypted = aes_cbc_with_iv:decrypt(decoded)
+        if decrypted then
+            return decrypted
+        end
+
+        if C.ERR_peek_error() then
+            -- clean up the error queue of OpenSSL to prevent
+            -- normal requests from being interfered with.
+            C.ERR_clear_error()
+        end
+    end
+
+    return nil, "decrypt " .. what .. "failed"
+end
+
+
+local _keyring
+local function get_keyring()
+    if _keyring == nil then
+        local local_conf = fetch_local_conf()
+        local ivs = tbl.try_read_attr(local_conf, "apisix", "data_encryption", 
"keyring")
+        _keyring = _M.init_iv_tbl(ivs)
+    end
+
+    return _keyring
+end
+
+
+--- Encrypt a plugin `encrypt_fields` value with the `data_encryption.keyring`.
+-- Returns `value` unchanged when no keyring is configured.
+function _M.encrypt(value)
+    local keyring = get_keyring()
+    if #keyring == 0 then
+        return value
+    end
+
+    local encrypted, err = _M.aes_cbc_encrypt(keyring, value)
+    if not encrypted then
+        log.error("failed to encrypt the data: ", err)
+        return value
+    end
+
+    return encrypted
+end
+
+
+--- Decrypt a plugin `encrypt_fields` value with the `data_encryption.keyring`.
+-- Returns `value` unchanged when no keyring is configured. `subject` 
(optional)
+-- is forwarded to qualify the error message.
+function _M.decrypt(value, subject)
+    local keyring = get_keyring()
+    if #keyring == 0 then
+        return value
+    end
+
+    return _M.aes_cbc_decrypt(keyring, value, subject)
+end
+
+
+return _M
diff --git a/apisix/plugin.lua b/apisix/plugin.lua
index 0a30410f7..da587e8e7 100644
--- a/apisix/plugin.lua
+++ b/apisix/plugin.lua
@@ -20,7 +20,6 @@ local config_util   = require("apisix.core.config_util")
 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 secret        = require("apisix.secret")
 
 local ngx           = ngx
@@ -1083,7 +1082,7 @@ local function process_encrypt_field(conf, key_path, 
operation, plugin_name, op_
         end
 
         if type(val) == "string" then
-            local result, err = operation(val, "data_encrypt")
+            local result, err = operation(val)
             if not result then
                 log_func("failed to ", op_name, " the conf of plugin [",
                          plugin_name, "] key [", key_path, "], err: ", err, 
hint)
@@ -1096,7 +1095,7 @@ local function process_encrypt_field(conf, key_path, 
operation, plugin_name, op_
                 -- array of strings
                 for i, item in ipairs(val) do
                     if type(item) == "string" then
-                        local result, err = operation(item, "data_encrypt")
+                        local result, err = operation(item)
                         if not result then
                             log_func("failed to ", op_name, " the conf of 
plugin [",
                                      plugin_name, "] key [", key_path,
@@ -1110,7 +1109,7 @@ local function process_encrypt_field(conf, key_path, 
operation, plugin_name, op_
                 -- map of strings
                 for k, v in pairs(val) do
                     if type(v) == "string" then
-                        local result, err = operation(v, "data_encrypt")
+                        local result, err = operation(v)
                         if not result then
                             log_func("failed to ", op_name, " the conf of 
plugin [",
                                      plugin_name, "] key [", key_path,
@@ -1161,7 +1160,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
-            process_encrypt_field(conf, key, apisix_ssl.aes_decrypt_pkey, 
name, "decrypt")
+            process_encrypt_field(conf, key, core.data_encryption.decrypt, 
name, "decrypt")
         end
     end
 end
@@ -1180,7 +1179,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
-            process_encrypt_field(conf, key, apisix_ssl.aes_encrypt_pkey, 
name, "encrypt")
+            process_encrypt_field(conf, key, core.data_encryption.encrypt, 
name, "encrypt")
         end
     end
 end
diff --git a/apisix/ssl.lua b/apisix/ssl.lua
index 42720b75a..9d0401fa2 100644
--- a/apisix/ssl.lua
+++ b/apisix/ssl.lua
@@ -16,26 +16,14 @@
 --
 local core           = require("apisix.core")
 local secret         = require("apisix.secret")
+local data_encryption = core.data_encryption
 local ngx_ssl        = require("ngx.ssl")
 local ngx_ssl_client = require("ngx.ssl.clienthello")
-local ffi            = require("ffi")
 
-local C = ffi.C
-local ngx_encode_base64 = ngx.encode_base64
-local ngx_decode_base64 = ngx.decode_base64
-local aes = require("resty.aes")
 local str_lower = string.lower
 local str_byte = string.byte
-local assert = assert
-local type = type
-local ipairs = ipairs
 local ngx_sub = ngx.re.sub
 
-ffi.cdef[[
-unsigned long ERR_peek_error(void);
-void ERR_clear_error(void);
-]]
-
 local cert_cache = core.lrucache.new {
     ttl = 3600, count = 1024,
 }
@@ -86,97 +74,23 @@ function _M.set_protocols_by_clienthello(ssl_protocols)
 end
 
 
-local function init_iv_tbl(ivs)
-    local _aes_128_cbc_with_iv_tbl = core.table.new(2, 0)
-    local type_ivs = type(ivs)
-
-    if type_ivs == "table" then
-        for _, iv in ipairs(ivs) do
-            local aes_with_iv = assert(aes:new(iv, nil, aes.cipher(128, 
"cbc"), {iv = iv}))
-            core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv)
-        end
-    elseif type_ivs == "string" then
-        local aes_with_iv = assert(aes:new(ivs, nil, aes.cipher(128, "cbc"), 
{iv = ivs}))
-        core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv)
-    end
-
-    return _aes_128_cbc_with_iv_tbl
-end
-
-
-local _aes_128_cbc_with_iv_tbl_gde
-local function get_aes_128_cbc_with_iv_gde(local_conf)
-    if _aes_128_cbc_with_iv_tbl_gde == nil then
-        local ivs = core.table.try_read_attr(local_conf, "apisix", 
"data_encryption", "keyring")
-        _aes_128_cbc_with_iv_tbl_gde = init_iv_tbl(ivs)
-    end
-
-    return _aes_128_cbc_with_iv_tbl_gde
-end
-
-
-
-local function encrypt(aes_128_cbc_with_iv, origin)
-    local encrypted = aes_128_cbc_with_iv:encrypt(origin)
-    if encrypted == nil then
-        core.log.error("failed to encrypt key")
+-- Encrypt an SSL private key with the data_encryption keyring. Only PEM-form
+-- keys are encrypted, so already-encrypted or non-PEM values pass through.
+function _M.aes_encrypt_pkey(origin)
+    if not core.string.has_prefix(origin, "---") then
         return origin
     end
 
-    return ngx_encode_base64(encrypted)
-end
-
-function _M.aes_encrypt_pkey(origin, field)
-    local local_conf = core.config.local_conf()
-    local aes_128_cbc_with_iv_tbl_gde = get_aes_128_cbc_with_iv_gde(local_conf)
-    local aes_128_cbc_with_iv_gde = aes_128_cbc_with_iv_tbl_gde[1]
-
-    if not field then
-        if aes_128_cbc_with_iv_gde ~= nil and core.string.has_prefix(origin, 
"---") then
-            return encrypt(aes_128_cbc_with_iv_gde, origin)
-        end
-    else
-        if field == "data_encrypt" then
-            if aes_128_cbc_with_iv_gde ~= nil then
-                return encrypt(aes_128_cbc_with_iv_gde, origin)
-            end
-        end
-    end
-    return origin
+    return data_encryption.encrypt(origin)
 end
 
 
-local function aes_decrypt_pkey(origin, field)
-    if not field and core.string.has_prefix(origin, "---") then
-        return origin
-    end
-
-    local local_conf = core.config.local_conf()
-    local aes_128_cbc_with_iv_tbl = get_aes_128_cbc_with_iv_gde(local_conf)
-    if #aes_128_cbc_with_iv_tbl == 0 then
+local function aes_decrypt_pkey(origin)
+    if core.string.has_prefix(origin, "---") then
         return origin
     end
 
-    local decoded_key = ngx_decode_base64(origin)
-    if not decoded_key then
-        core.log.error("base64 decode ssl key failed")
-        return nil, "base64 decode ssl key failed"
-    end
-
-    for _, aes_128_cbc_with_iv in ipairs(aes_128_cbc_with_iv_tbl) do
-        local decrypted = aes_128_cbc_with_iv:decrypt(decoded_key)
-        if decrypted then
-            return decrypted
-        end
-
-        if C.ERR_peek_error() then
-            -- clean up the error queue of OpenSSL to prevent
-            -- normal requests from being interfered with.
-            C.ERR_clear_error()
-        end
-    end
-
-    return nil, "decrypt ssl key failed"
+    return data_encryption.decrypt(origin, "ssl key")
 end
 _M.aes_decrypt_pkey = aes_decrypt_pkey
 
diff --git a/t/node/data_encrypt.t b/t/node/data_encrypt.t
index d166cb480..fceee1d56 100644
--- a/t/node/data_encrypt.t
+++ b/t/node/data_encrypt.t
@@ -334,7 +334,7 @@ apisix:
 bar
 bar
 --- error_log
-failed to decrypt the conf of plugin [basic-auth] key [password], err: decrypt 
ssl key failed
+failed to decrypt the conf of plugin [basic-auth] key [password], err: decrypt 
failed
 --- no_error_log
 key\[bar\]
 
@@ -396,7 +396,7 @@ auth-two
 bar
 vU/ZHVJw7b0XscDJ1Fhtig==
 --- error_log
-failed to decrypt the conf of plugin [basic-auth] key [password], err: decrypt 
ssl key failed
+failed to decrypt the conf of plugin [basic-auth] key [password], err: decrypt 
failed
 --- no_error_log
 key\[bar\]
 
diff --git a/t/node/data_encrypt3.t b/t/node/data_encrypt3.t
index 407c276a6..84906d1ed 100644
--- a/t/node/data_encrypt3.t
+++ b/t/node/data_encrypt3.t
@@ -503,7 +503,7 @@ apisix:
     location /t {
         content_by_lua_block {
             local plugin = require("apisix.plugin")
-            local ssl = require("apisix.ssl")
+            local core = require("apisix.core")
 
             -- Simulate array-of-strings encryption (e.g., secret_fallbacks)
             local conf = {
@@ -511,7 +511,7 @@ apisix:
             }
 
             -- Encrypt
-            plugin.process_encrypt_field(conf, "secrets", 
ssl.aes_encrypt_pkey, "test", "encrypt")
+            plugin.process_encrypt_field(conf, "secrets", 
core.data_encryption.encrypt, "test", "encrypt")
 
             -- Verify all elements are encrypted (not plaintext)
             for i, v in ipairs(conf.secrets) do
@@ -520,7 +520,7 @@ apisix:
             end
 
             -- Decrypt
-            plugin.process_encrypt_field(conf, "secrets", 
ssl.aes_decrypt_pkey, "test", "decrypt")
+            plugin.process_encrypt_field(conf, "secrets", 
core.data_encryption.decrypt, "test", "decrypt")
 
             -- Verify all elements are restored
             ngx.say("decrypted[1]: ", conf.secrets[1])
@@ -535,3 +535,30 @@ encrypted[3] differs: true
 decrypted[1]: secret-one
 decrypted[2]: secret-two
 decrypted[3]: secret-three
+
+
+
+=== TEST 8: data_encryption.decrypt of a non-base64 value returns the error to 
the caller without self-logging it
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+
+            -- A value that was stored as plaintext before its field was added 
to
+            -- encrypt_fields: it is not valid base64, so decryption cannot 
decode it.
+            local val, err = core.data_encryption.decrypt("sk-proj-abc-123!")
+            ngx.say("val: ", val)
+            ngx.say("err: ", err)
+        }
+    }
+--- response_body
+val: nil
+err: base64 decode failed
+--- no_error_log
+base64 decode failed

Reply via email to