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