- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
280 lines
7.3 KiB
Lua
280 lines
7.3 KiB
Lua
--
|
|
-- 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.
|
|
--
|
|
local core = require("apisix.core")
|
|
local consumer_mod = require("apisix.consumer")
|
|
local base64 = require("ngx.base64")
|
|
local aes = require("resty.aes")
|
|
local ngx = ngx
|
|
local sub_str = string.sub
|
|
local cipher = aes.cipher(256, "gcm")
|
|
|
|
local plugin_name = "jwe-decrypt"
|
|
|
|
local schema = {
|
|
type = "object",
|
|
properties = {
|
|
header = {
|
|
type = "string",
|
|
default = "Authorization"
|
|
},
|
|
forward_header = {
|
|
type = "string",
|
|
default = "Authorization"
|
|
},
|
|
strict = {
|
|
type = "boolean",
|
|
default = true
|
|
}
|
|
},
|
|
required = { "header", "forward_header" },
|
|
}
|
|
|
|
local consumer_schema = {
|
|
type = "object",
|
|
properties = {
|
|
key = { type = "string" },
|
|
secret = { type = "string" },
|
|
is_base64_encoded = { type = "boolean" },
|
|
},
|
|
required = { "key", "secret" },
|
|
encrypt_fields = { "key", "secret" },
|
|
}
|
|
|
|
|
|
local _M = {
|
|
version = 0.1,
|
|
priority = 2509,
|
|
type = 'auth',
|
|
name = plugin_name,
|
|
schema = schema,
|
|
consumer_schema = consumer_schema
|
|
}
|
|
|
|
|
|
function _M.check_schema(conf, schema_type)
|
|
if schema_type == core.schema.TYPE_CONSUMER then
|
|
local ok, err = core.schema.check(consumer_schema, conf)
|
|
if not ok then
|
|
return false, err
|
|
end
|
|
|
|
local local_conf, err = core.config.local_conf(true)
|
|
if not local_conf then
|
|
return false, "failed to load the configuration file: " .. err
|
|
end
|
|
|
|
local encrypted = core.table.try_read_attr(local_conf, "apisix", "data_encryption",
|
|
"enable_encrypt_fields") and (core.config.type == "etcd")
|
|
|
|
-- if encrypted, the secret length will exceed 32 so don't check
|
|
if not encrypted then
|
|
-- restrict the length of secret, we use A256GCM for encryption,
|
|
-- so the length should be 32 chars only
|
|
if conf.is_base64_encoded then
|
|
if #base64.decode_base64url(conf.secret) ~= 32 then
|
|
return false, "the secret length after base64 decode should be 32 chars"
|
|
end
|
|
else
|
|
if #conf.secret ~= 32 then
|
|
return false, "the secret length should be 32 chars"
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
return core.schema.check(schema, conf)
|
|
end
|
|
|
|
|
|
local function get_secret(conf)
|
|
local secret = conf.secret
|
|
|
|
if conf.is_base64_encoded then
|
|
return base64.decode_base64url(secret)
|
|
end
|
|
|
|
return secret
|
|
end
|
|
|
|
|
|
local function load_jwe_token(jwe_token)
|
|
local o = { valid = false }
|
|
o.header, o.enckey, o.iv, o.ciphertext, o.tag = jwe_token:match("(.-)%.(.-)%.(.-)%.(.-)%.(.*)")
|
|
if not o.header then
|
|
return o
|
|
end
|
|
local he = base64.decode_base64url(o.header)
|
|
if not he then
|
|
return o
|
|
end
|
|
o.header_obj = core.json.decode(he)
|
|
if not o.header_obj then
|
|
return o
|
|
end
|
|
o.valid = true
|
|
return o
|
|
end
|
|
|
|
|
|
local function jwe_decrypt_with_obj(o, consumer)
|
|
local secret = get_secret(consumer.auth_conf)
|
|
local dec = base64.decode_base64url
|
|
|
|
local aes_default = aes:new(
|
|
secret,
|
|
nil,
|
|
cipher,
|
|
{iv = dec(o.iv)}
|
|
)
|
|
|
|
local decrypted = aes_default:decrypt(dec(o.ciphertext), dec(o.tag))
|
|
return decrypted
|
|
end
|
|
|
|
|
|
local function jwe_encrypt(o, consumer)
|
|
local secret = get_secret(consumer.auth_conf)
|
|
local enc = base64.encode_base64url
|
|
|
|
local aes_default = aes:new(
|
|
secret,
|
|
nil,
|
|
cipher,
|
|
{iv = o.iv})
|
|
|
|
local encrypted = aes_default:encrypt(o.plaintext)
|
|
|
|
o.ciphertext = encrypted[1]
|
|
o.tag = encrypted[2]
|
|
return o.header .. ".." .. enc(o.iv) .. "." .. enc(o.ciphertext) .. "." .. enc(o.tag)
|
|
end
|
|
|
|
|
|
local function get_consumer(key)
|
|
local consumer_conf = consumer_mod.plugin(plugin_name)
|
|
if not consumer_conf then
|
|
return nil
|
|
end
|
|
local consumers = consumer_mod.consumers_kv(plugin_name, consumer_conf, "key")
|
|
if not consumers then
|
|
return nil
|
|
end
|
|
core.log.info("consumers: ", core.json.delay_encode(consumers))
|
|
return consumers[key]
|
|
end
|
|
|
|
|
|
local function fetch_jwe_token(conf, ctx)
|
|
local token = core.request.header(ctx, conf.header)
|
|
if token then
|
|
local prefix = sub_str(token, 1, 7)
|
|
if prefix == 'Bearer ' or prefix == 'bearer ' then
|
|
return sub_str(token, 8)
|
|
end
|
|
|
|
return token
|
|
end
|
|
end
|
|
|
|
|
|
function _M.rewrite(conf, ctx)
|
|
-- fetch token and hide credentials if necessary
|
|
local jwe_token, err = fetch_jwe_token(conf, ctx)
|
|
if not jwe_token and conf.strict then
|
|
core.log.info("failed to fetch JWE token: ", err)
|
|
return 403, { message = "missing JWE token in request" }
|
|
end
|
|
|
|
local jwe_obj = load_jwe_token(jwe_token)
|
|
if not jwe_obj.valid then
|
|
return 400, { message = "JWE token invalid" }
|
|
end
|
|
|
|
if not jwe_obj.header_obj.kid then
|
|
return 400, { message = "missing kid in JWE token" }
|
|
end
|
|
|
|
local consumer = get_consumer(jwe_obj.header_obj.kid)
|
|
if not consumer then
|
|
return 400, { message = "invalid kid in JWE token" }
|
|
end
|
|
|
|
local plaintext, err = jwe_decrypt_with_obj(jwe_obj, consumer)
|
|
if err ~= nil then
|
|
return 400, { message = "failed to decrypt JWE token" }
|
|
end
|
|
core.request.set_header(ctx, conf.forward_header, plaintext)
|
|
end
|
|
|
|
|
|
local function gen_token()
|
|
local args = core.request.get_uri_args()
|
|
if not args or not args.key then
|
|
return core.response.exit(400)
|
|
end
|
|
|
|
local key = args.key
|
|
local payload = args.payload
|
|
if payload then
|
|
payload = ngx.unescape_uri(payload)
|
|
end
|
|
|
|
local consumer = get_consumer(key)
|
|
if not consumer then
|
|
return core.response.exit(404)
|
|
end
|
|
|
|
core.log.info("consumer: ", core.json.delay_encode(consumer))
|
|
|
|
local iv = args.iv
|
|
if not iv then
|
|
-- TODO: random bytes
|
|
iv = "123456789012"
|
|
end
|
|
|
|
local obj = {
|
|
iv = iv,
|
|
plaintext = payload,
|
|
header_obj = {
|
|
kid = key,
|
|
alg = "dir",
|
|
enc = "A256GCM",
|
|
},
|
|
}
|
|
obj.header = base64.encode_base64url(core.json.encode(obj.header_obj))
|
|
local jwe_token = jwe_encrypt(obj, consumer)
|
|
if jwe_token then
|
|
return core.response.exit(200, jwe_token)
|
|
end
|
|
|
|
return core.response.exit(404)
|
|
end
|
|
|
|
|
|
function _M.api()
|
|
return {
|
|
{
|
|
methods = { "GET" },
|
|
uri = "/apisix/plugin/jwe/encrypt",
|
|
handler = gen_token,
|
|
}
|
|
}
|
|
end
|
|
|
|
return _M
|