Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions apisix/cli/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
}
},
}
Expand Down
16 changes: 13 additions & 3 deletions apisix/core/data_encryption.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions conf/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/en/latest/plugin-develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/latest/plugin-develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 都无法解密数据,则使用原始数据。
Expand Down
32 changes: 32 additions & 0 deletions t/cli/test_main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
155 changes: 155 additions & 0 deletions t/core/data_encryption.t
Original file line number Diff line number Diff line change
@@ -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]
Loading