This is an automated email from the ASF dual-hosted git repository.
AlinsRan 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 e22bb1c034 feat(data_encryption): support AES-256 keys in the
encryption keyring (#13756)
e22bb1c034 is described below
commit e22bb1c034268d98aae5feedec02a78c49acd9e0
Author: AlinsRan <[email protected]>
AuthorDate: Thu Aug 13 18:02:13 2026 +0800
feat(data_encryption): support AES-256 keys in the encryption keyring
(#13756)
---
apisix/cli/schema.lua | 24 +++---
apisix/core/data_encryption.lua | 16 +++-
conf/config.yaml.example | 4 +-
docs/en/latest/plugin-develop.md | 2 +
docs/zh/latest/plugin-develop.md | 2 +
t/cli/test_main.sh | 32 ++++++++
t/core/data_encryption.t | 155 +++++++++++++++++++++++++++++++++++++++
7 files changed, 220 insertions(+), 15 deletions(-)
diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua
index 3e3e855106..25c4cc5076 100644
--- a/apisix/cli/schema.lua
+++ b/apisix/cli/schema.lua
@@ -67,6 +67,18 @@ local etcd_schema = {
required = {"prefix", "host"}
}
+-- a keyring entry is used both as the AES key and as its IV, so only the two
+-- key lengths AES-CBC accepts here are allowed: 16 bytes (AES-128) and 32
+-- bytes (AES-256). Any other length would be dropped by
+-- core.data_encryption.init_iv_tbl and silently leave the data unencrypted.
+local keyring_key_schema = {
+ type = "string",
+ anyOf = {
+ {minLength = 16, maxLength = 16},
+ {minLength = 32, maxLength = 32},
+ }
+}
+
local config_schema = {
type = "object",
properties = {
@@ -246,17 +258,9 @@ local config_schema = {
{
type = "array",
minItems = 1,
- items = {
- type = "string",
- minLength = 16,
- maxLength = 16
- }
+ items = keyring_key_schema
},
- {
- type = "string",
- minLength = 16,
- maxLength = 16
- }
+ keyring_key_schema
}
},
}
diff --git a/apisix/core/data_encryption.lua b/apisix/core/data_encryption.lua
index 88bbaf1074..f2394272b6 100644
--- a/apisix/core/data_encryption.lua
+++ b/apisix/core/data_encryption.lua
@@ -42,8 +42,11 @@ 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.
+--- Build a table of AES-CBC ciphers from a keyring, each using the key itself
+-- as the IV: a 16-byte key selects AES-128, a 32-byte key selects AES-256.
+-- The CLI schema rejects any other length at startup; a key that still reaches
+-- us with an unsupported length is dropped, so log it loudly -- an empty
+-- keyring makes `_M.encrypt` fall back to storing the value in clear text.
function _M.init_iv_tbl(ivs)
local iv_tbl = tbl.new(2, 0)
if type(ivs) == "string" then
@@ -52,7 +55,14 @@ function _M.init_iv_tbl(ivs)
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})))
+ if #iv == 32 then
+ tbl.insert(iv_tbl, assert(aes:new(iv, nil, aes.cipher(256,
"cbc"), {iv = iv})))
+ elseif #iv == 16 then
+ tbl.insert(iv_tbl, assert(aes:new(iv, nil, aes.cipher(128,
"cbc"), {iv = iv})))
+ else
+ log.error("ignored data_encryption keyring entry: expected a
16 byte ",
+ "(AES-128) or 32 byte (AES-256) key, got ", #iv, "
bytes")
+ end
end
end
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index bcc5e6acb4..ebdc1535f4 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -144,8 +144,8 @@ apisix:
enable_encrypt_fields: true # Whether enable encrypt fields specified in
`encrypt_fields` in plugin schema.
keyring: # This field is used to encrypt the private
key of SSL and the `encrypt_fields`
# in plugin schema.
- - qeddd145sfvddff3 # Set the encryption key for AES-128-CBC. It
should be a hexadecimal string
- # of length 16.
+ - qeddd145sfvddff3 # Set the encryption key. A key of length 16
selects AES-128-CBC and a key
+ # of length 32 selects AES-256-CBC; any
other length is rejected on startup.
- edd1c9f0985e76a2 # If not set, APISIX saves the original data
into etcd.
# CAUTION: If you would like to update the
key, add the new key as the
# first item in the array and keep the older
keys below the newly added
diff --git a/docs/en/latest/plugin-develop.md b/docs/en/latest/plugin-develop.md
index deadabe81d..cdf837cc75 100644
--- a/docs/en/latest/plugin-develop.md
+++ b/docs/en/latest/plugin-develop.md
@@ -356,6 +356,8 @@ apisix:
- ...
```
+Each key doubles as the IV, so its length selects the cipher: a key of 16
characters uses AES-128-CBC and a key of 32 characters uses AES-256-CBC. Keys
of any other length are rejected when APISIX starts. Keys of both lengths can
be mixed in one keyring, which is what makes it possible to rotate an AES-128
keyring to AES-256 without losing access to the already encrypted data.
+
APISIX will try to decrypt the data with keys in the order of the keys in the
keyring (only for parameters declared in `encrypt_fields`). If the decryption
fails, the next key will be tried until the decryption succeeds.
If none of the keys in `keyring` can decrypt the data, the original data is
used.
diff --git a/docs/zh/latest/plugin-develop.md b/docs/zh/latest/plugin-develop.md
index 907184372b..8fa2c0d4e0 100644
--- a/docs/zh/latest/plugin-develop.md
+++ b/docs/zh/latest/plugin-develop.md
@@ -342,6 +342,8 @@ apisix:
- ...
```
+每个 key 同时用作 IV,因此 key 的长度决定了使用的算法:长度为 16 的 key 使用 AES-128-CBC,长度为 32 的 key 使用
AES-256-CBC,其它长度的 key 会在 APISIX 启动时被拒绝。同一个 keyring 中可以混用这两种长度的
key,这样才能在不丢失已加密数据的前提下,把 AES-128 的 keyring 轮转为 AES-256。
+
`keyring` 是一个数组,可以指定多个 key,APISIX 会按照 keyring 中 key 的顺序,依次尝试用 key 来解密数据(只对在
`encrypt_fields` 声明的参数)。如果解密失败,会尝试下一个 key,直到解密成功。
如果 `keyring` 中的 key 都无法解密数据,则使用原始数据。
diff --git a/t/cli/test_main.sh b/t/cli/test_main.sh
index c0e7d90c09..c6c00adf23 100755
--- a/t/cli/test_main.sh
+++ b/t/cli/test_main.sh
@@ -303,6 +303,38 @@ fi
echo "passed: env value quoting (#11467)"
+# a 32 byte data encryption key selects AES-256 and must pass config validation
+cat > conf/config.yaml <<'EOF'
+apisix:
+ data_encryption:
+ keyring:
+ - qeddd145sfvddff3qeddd145sfvddff3
+ - qeddd145sfvddff3
+EOF
+
+out=$(make init 2>&1 || true)
+if echo "$out" | grep "failed to validate config"; then
+ echo "failed: a 32 byte data encryption keyring should be accepted"
+ exit 1
+fi
+
+# a key that is neither 16 nor 32 bytes would be dropped at runtime and
silently
+# leave the data unencrypted, so it has to be rejected on startup
+cat > conf/config.yaml <<'EOF'
+apisix:
+ data_encryption:
+ keyring:
+ - qeddd145sfvddff3qedd
+EOF
+
+out=$(make init 2>&1 || true)
+if ! echo "$out" | grep "failed to validate config"; then
+ echo "failed: a data encryption keyring of an unsupported length should be
rejected"
+ exit 1
+fi
+
+echo "passed: data encryption keyring length validation"
+
# support environment variables
echo '
nginx_config:
diff --git a/t/core/data_encryption.t b/t/core/data_encryption.t
new file mode 100644
index 0000000000..7c73eea6bf
--- /dev/null
+++ b/t/core/data_encryption.t
@@ -0,0 +1,155 @@
+#
+# 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(2);
+no_long_string();
+no_root_location();
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: 16 byte key builds an AES-128 cipher and round-trips
+--- config
+ location /t {
+ content_by_lua_block {
+ local data_encryption = require("apisix.core.data_encryption")
+ local iv_tbl = data_encryption.init_iv_tbl("qeddd145sfvddff3")
+ ngx.say("ciphers: ", #iv_tbl)
+
+ local enc = data_encryption.aes_cbc_encrypt(iv_tbl, "hello world")
+ ngx.say("encrypted differs: ", enc ~= "hello world")
+ ngx.say("decrypted: ", data_encryption.aes_cbc_decrypt(iv_tbl,
enc))
+ }
+ }
+--- request
+GET /t
+--- response_body
+ciphers: 1
+encrypted differs: true
+decrypted: hello world
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: 32 byte key builds an AES-256 cipher and round-trips
+--- config
+ location /t {
+ content_by_lua_block {
+ local data_encryption = require("apisix.core.data_encryption")
+ local iv_tbl =
data_encryption.init_iv_tbl("qeddd145sfvddff3qeddd145sfvddff3")
+ ngx.say("ciphers: ", #iv_tbl)
+
+ local enc = data_encryption.aes_cbc_encrypt(iv_tbl, "hello world")
+ ngx.say("encrypted differs: ", enc ~= "hello world")
+ ngx.say("decrypted: ", data_encryption.aes_cbc_decrypt(iv_tbl,
enc))
+ }
+ }
+--- request
+GET /t
+--- response_body
+ciphers: 1
+encrypted differs: true
+decrypted: hello world
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: rotating an AES-128 keyring to AES-256 keeps the old data readable
+--- config
+ location /t {
+ content_by_lua_block {
+ local data_encryption = require("apisix.core.data_encryption")
+
+ -- data written before the rotation, encrypted with the 16 byte
key only
+ local legacy = data_encryption.init_iv_tbl("qeddd145sfvddff3")
+ local legacy_enc = data_encryption.aes_cbc_encrypt(legacy, "hello
world")
+
+ -- the new 32 byte key goes first, the old one is kept to read old
data
+ local rotated = data_encryption.init_iv_tbl({
+ "qeddd145sfvddff3qeddd145sfvddff3",
+ "qeddd145sfvddff3",
+ })
+ ngx.say("ciphers: ", #rotated)
+ ngx.say("legacy data: ", data_encryption.aes_cbc_decrypt(rotated,
legacy_enc))
+
+ -- new writes go through the first (AES-256) cipher
+ local enc = data_encryption.aes_cbc_encrypt(rotated, "hello world")
+ ngx.say("re-encrypted: ", enc ~= legacy_enc)
+ ngx.say("new data: ", data_encryption.aes_cbc_decrypt(rotated,
enc))
+ }
+ }
+--- request
+GET /t
+--- response_body
+ciphers: 2
+legacy data: hello world
+re-encrypted: true
+new data: hello world
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: a key of an unsupported length is dropped and reported
+--- config
+ location /t {
+ content_by_lua_block {
+ local data_encryption = require("apisix.core.data_encryption")
+ local iv_tbl = data_encryption.init_iv_tbl({
+ "short",
+ "qeddd145sfvddff3",
+ })
+ ngx.say("ciphers: ", #iv_tbl)
+ }
+ }
+--- request
+GET /t
+--- response_body
+ciphers: 1
+--- error_log
+expected a 16 byte (AES-128) or 32 byte (AES-256) key, got 5 bytes
+
+
+
+=== TEST 5: a 32 byte keyring configured in config.yaml encrypts and decrypts
+--- yaml_config
+apisix:
+ node_listen: 1984
+ data_encryption:
+ enable_encrypt_fields: true
+ keyring:
+ - qeddd145sfvddff3qeddd145sfvddff3
+--- config
+ location /t {
+ content_by_lua_block {
+ local data_encryption = require("apisix.core.data_encryption")
+ local enc = data_encryption.encrypt("hello world")
+ ngx.say("encrypted: ", enc ~= "hello world")
+ ngx.say("decrypted: ", data_encryption.decrypt(enc))
+ }
+ }
+--- request
+GET /t
+--- response_body
+encrypted: true
+decrypted: hello world
+--- no_error_log
+[error]