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]

Reply via email to