mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
git-subtree-dir: src/deps/src/lua-resty-session git-subtree-split: 8b5f8752f3046396c414c5b97850e784c07e1641
1121 lines
30 KiB
Lua
1121 lines
30 KiB
Lua
---
|
|
-- Common utilities for session library and storage backends
|
|
--
|
|
-- @module resty.session.utils
|
|
|
|
|
|
local require = require
|
|
|
|
|
|
local buffer = require "string.buffer"
|
|
local bit = require "bit"
|
|
|
|
|
|
local select = select
|
|
local floor = math.floor
|
|
local ceil = math.ceil
|
|
local byte = string.byte
|
|
local char = string.char
|
|
local band = bit.band
|
|
local bnot = bit.bnot
|
|
local bor = bit.bor
|
|
local fmt = string.format
|
|
local sub = string.sub
|
|
|
|
|
|
local is_fips_mode do
|
|
local IS_FIPS
|
|
|
|
local function is_fips_mode_real()
|
|
return IS_FIPS == true
|
|
end
|
|
|
|
---
|
|
-- Returns whether OpenSSL is in FIPS-mode.
|
|
--
|
|
-- @function utils.is_fips_mode
|
|
-- @treturn boolean `true` if OpenSSL is in FIPS-mode, otherwise `false`
|
|
--
|
|
-- @usage
|
|
-- local is_fips = require "resty.session.utils".is_fips_mode()
|
|
is_fips_mode = function()
|
|
IS_FIPS = require("resty.openssl").get_fips_mode()
|
|
is_fips_mode = is_fips_mode_real
|
|
return is_fips_mode()
|
|
end
|
|
end
|
|
|
|
|
|
local bpack, bunpack do
|
|
local buf = buffer.new()
|
|
|
|
---
|
|
-- Binary pack unsigned integer.
|
|
--
|
|
-- Returns binary packed version of an integer in little endian unsigned format.
|
|
--
|
|
-- Size can be:
|
|
--
|
|
-- * `1`, pack input as a little endian unsigned char (`<C`)
|
|
-- * `2`, pack input as a little endian unsigned short (`<S`)
|
|
-- * `3`, pack input as a little endian unsigned integer (truncated) (`<I`)
|
|
-- * `4`, pack input as a little endian unsigned integer (`<I`)
|
|
-- * `5`, pack input as a little endian unsigned long (truncated) (`<L`)
|
|
-- * `6`, pack input as a little endian unsigned long (truncated) (`<L`)
|
|
-- * `7`, pack input as a little endian unsigned long (truncated) (`<L`)
|
|
-- * `8`, pack input as a little endian unsigned long (`<L`)
|
|
--
|
|
-- @function utils.bpack
|
|
-- @tparam number size size of binary packed output
|
|
-- @tparam number value value to binary pack
|
|
-- @treturn string binary packed value
|
|
--
|
|
-- @usage
|
|
-- local packed_128 = require "resty.session.utils".bpack(1, 128)
|
|
-- local packed_now = require "resty.session.utils".bpack(8, ngx.time())
|
|
bpack = function(size, value)
|
|
buf:reset()
|
|
for i = 1, size do
|
|
buf:put(char(value % 256))
|
|
if i == size then
|
|
return buf:get()
|
|
end
|
|
value = floor(value / 256)
|
|
end
|
|
end
|
|
|
|
---
|
|
-- Binary unpack unsigned integer.
|
|
--
|
|
-- Returns number from a little endian unsigned binary packed format.
|
|
--
|
|
-- Size can be:
|
|
--
|
|
-- * `1`, unpack input from little endian unsigned char (`<C`)
|
|
-- * `2`, unpack input from little endian unsigned short (`<S`)
|
|
-- * `3`, unpack input from little endian unsigned integer (truncated) (`<I`)
|
|
-- * `4`, unpack input from little endian unsigned integer (`<I`)
|
|
-- * `5`, unpack input from little endian unsigned integer (truncated) (`<L`)
|
|
-- * `6`, unpack input from little endian unsigned integer (truncated) (`<L`)
|
|
-- * `7`, unpack input from little endian unsigned integer (truncated) (`<L`)
|
|
-- * `8`, unpack input from little endian unsigned long (`<L`)
|
|
--
|
|
-- @function utils.bunpack
|
|
-- @tparam number size size of binary packed output
|
|
-- @tparam number value value to binary pack
|
|
-- @treturn number binary unpacked value
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local value = 128
|
|
-- local packed_value = utils.bpack(1, value)
|
|
-- local unpacked_value = utils.bunpack(1, packed_value)
|
|
-- print(value == unpacked_value) -- true
|
|
bunpack = function(size, value)
|
|
buf:set(value)
|
|
value = 0
|
|
for i = 1, size do
|
|
value = value + byte(buf:get(1)) * (2 ^ ((i - 1) * 8))
|
|
end
|
|
return value
|
|
end
|
|
end
|
|
|
|
|
|
local trim do
|
|
local SPACE_BYTE = byte(" ")
|
|
local TAB_BYTE = byte("\t")
|
|
local CR_BYTE = byte("\r")
|
|
local LF_BYTE = byte("\n")
|
|
local VTAB_BYTE = byte("\v")
|
|
local FF_BYTE = byte("\f")
|
|
|
|
---
|
|
-- Trim whitespace from the start and from the end of string.
|
|
--
|
|
-- Characters that are trimmed:
|
|
--
|
|
-- * space `" "`
|
|
-- * tab `"\t"`
|
|
-- * carriage return `"\r"`
|
|
-- * line feed `"\n"`
|
|
-- * vertical tab `"\v"`
|
|
-- * form feed `"\f"`
|
|
--
|
|
-- @function utils.trim
|
|
-- @tparam string value string to trim
|
|
-- @treturn string a whitespace trimmed string
|
|
--
|
|
-- @usage
|
|
-- local trimmed = require "resty.session.utils".trim(" hello world ")
|
|
trim = function(value)
|
|
if value == nil or value == "" then
|
|
return ""
|
|
end
|
|
|
|
local len = #value
|
|
|
|
local s = 1
|
|
for i = 1, len do
|
|
local b = byte(value, i)
|
|
if b == SPACE_BYTE
|
|
or b == TAB_BYTE
|
|
or b == CR_BYTE
|
|
or b == LF_BYTE
|
|
or b == VTAB_BYTE
|
|
or b == FF_BYTE
|
|
then
|
|
s = s + 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
local e = len
|
|
for i = len, 1, -1 do
|
|
local b = byte(value, i)
|
|
if b == SPACE_BYTE
|
|
or b == TAB_BYTE
|
|
or b == CR_BYTE
|
|
or b == LF_BYTE
|
|
or b == VTAB_BYTE
|
|
or b == FF_BYTE
|
|
then
|
|
e = e - 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
if s ~= 1 or e ~= len then
|
|
return sub(value, s, e)
|
|
end
|
|
|
|
return value
|
|
end
|
|
end
|
|
|
|
|
|
local encode_json, decode_json do
|
|
local cjson
|
|
|
|
---
|
|
-- JSON encode value.
|
|
--
|
|
-- @function utils.encode_json
|
|
-- @tparam any value value to json encode
|
|
-- @treturn string json encoded value
|
|
--
|
|
-- @usage
|
|
-- local json = require "resty.session.utils".encode_json({ hello = "world" })
|
|
encode_json = function(value)
|
|
if not cjson then
|
|
cjson = require "cjson.safe".new()
|
|
end
|
|
encode_json = cjson.encode
|
|
return encode_json(value)
|
|
end
|
|
|
|
---
|
|
-- JSON decode value.
|
|
--
|
|
-- @function utils.decode_json
|
|
-- @tparam string value string to json decode
|
|
-- @treturn any json decoded value
|
|
--
|
|
-- @usage
|
|
-- local tbl = require "resty.session.utils".decode_json('{ "hello": "world" }')
|
|
decode_json = function(value)
|
|
if not cjson then
|
|
cjson = require "cjson.safe".new()
|
|
end
|
|
decode_json = cjson.decode
|
|
return decode_json(value)
|
|
end
|
|
end
|
|
|
|
|
|
local encode_base64url, decode_base64url, base64_size do
|
|
local base64
|
|
|
|
---
|
|
-- Base64 URL encode value.
|
|
--
|
|
-- @function utils.encode_base64url
|
|
-- @tparam string value string to base64 url encode
|
|
-- @treturn string base64 url encoded value
|
|
--
|
|
-- @usage
|
|
-- local encoded = require "resty.session.utils".encode_base64url("test")
|
|
encode_base64url = function(value)
|
|
if not base64 then
|
|
base64 = require "ngx.base64"
|
|
end
|
|
encode_base64url = base64.encode_base64url
|
|
return encode_base64url(value)
|
|
end
|
|
|
|
---
|
|
-- Base64 URL decode value
|
|
--
|
|
-- @function utils.decode_base64url
|
|
-- @tparam string value string to base64 url decode
|
|
-- @treturn string base64 url decoded value
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local encoded = utils.encode_base64url("test")
|
|
-- local decoded = utils.decode_base64url(encoded)
|
|
decode_base64url = function(value)
|
|
if not base64 then
|
|
base64 = require "ngx.base64"
|
|
end
|
|
decode_base64url = base64.decode_base64url
|
|
return decode_base64url(value)
|
|
end
|
|
|
|
---
|
|
-- Base64 size from original size (without padding).
|
|
--
|
|
-- @function utils.base64_size
|
|
-- @tparam number size original size
|
|
-- @treturn number base64 url encoded size without padding
|
|
--
|
|
-- @usage
|
|
-- local test = "test"
|
|
-- local b64len = require "resty.session.utils".base64_size(#test)
|
|
base64_size = function(size)
|
|
return ceil(4 * size / 3)
|
|
end
|
|
end
|
|
|
|
|
|
local deflate, inflate do
|
|
local DEFLATE_WINDOW_BITS = -15
|
|
local DEFLATE_CHUNK_SIZE = 8192
|
|
local DEFLATE_OPTIONS = {
|
|
windowBits = DEFLATE_WINDOW_BITS,
|
|
}
|
|
|
|
local zlib
|
|
local input_buffer = buffer.new()
|
|
local output_buffer = buffer.new()
|
|
|
|
local function prepare_buffers(input)
|
|
input_buffer:set(input)
|
|
output_buffer:reset()
|
|
end
|
|
|
|
local function read_input_buffer(size)
|
|
local data = input_buffer:get(size)
|
|
return data ~= "" and data or nil
|
|
end
|
|
|
|
local function write_output_buffer(data)
|
|
return output_buffer:put(data)
|
|
end
|
|
|
|
local function gzip(inflate_or_deflate, input, chunk_size, window_bits_or_options)
|
|
prepare_buffers(input)
|
|
local ok, err = inflate_or_deflate(read_input_buffer, write_output_buffer,
|
|
chunk_size, window_bits_or_options)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
return output_buffer:get()
|
|
end
|
|
|
|
local function deflate_real(data)
|
|
return gzip(zlib.deflateGzip, data, DEFLATE_CHUNK_SIZE, DEFLATE_OPTIONS)
|
|
end
|
|
|
|
local function inflate_real(data)
|
|
return gzip(zlib.inflateGzip, data, DEFLATE_CHUNK_SIZE, DEFLATE_WINDOW_BITS)
|
|
end
|
|
|
|
|
|
---
|
|
-- Compress the data with deflate algorithm.
|
|
--
|
|
-- @function utils.deflate
|
|
-- @tparam string data data to deflate
|
|
-- @treturn string deflated data
|
|
--
|
|
-- @usage
|
|
-- local test = "test"
|
|
-- local deflated = require "resty.session.utils".deflate(("a"):rep(100))
|
|
deflate = function(data)
|
|
if not zlib then
|
|
zlib = require "ffi-zlib"
|
|
end
|
|
deflate = deflate_real
|
|
return deflate(data)
|
|
end
|
|
|
|
---
|
|
-- Decompress the data compressed with deflate algorithm.
|
|
--
|
|
-- @function utils.inflate
|
|
-- @tparam string data data to inflate
|
|
-- @treturn string inflated data
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local deflated = utils.deflate(("a"):rep(100))
|
|
-- local inflated = utils.inflate(deflated)
|
|
inflate = function(data)
|
|
if not zlib then
|
|
zlib = require "ffi-zlib"
|
|
end
|
|
inflate = inflate_real
|
|
return inflate(data)
|
|
end
|
|
end
|
|
|
|
|
|
local rand_bytes do
|
|
local rand
|
|
|
|
---
|
|
-- Generate crypto random bytes.
|
|
--
|
|
-- @function utils.rand_bytes
|
|
-- @tparam number length how many bytes of random data to generate
|
|
-- @treturn string|nil random bytes
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local bytes = require "resty.session.utils".rand_bytes(32)
|
|
rand_bytes = function(length)
|
|
if not rand then
|
|
rand = require "resty.openssl.rand"
|
|
end
|
|
rand_bytes = rand.bytes
|
|
return rand_bytes(length)
|
|
end
|
|
end
|
|
|
|
|
|
local sha256 do
|
|
local digest
|
|
local sha256_digest
|
|
|
|
local function sha256_real(value)
|
|
local _, err, output
|
|
if not sha256_digest then
|
|
sha256_digest, err = digest.new("sha256")
|
|
if err then
|
|
return nil, err
|
|
end
|
|
end
|
|
|
|
output, err = sha256_digest:final(value)
|
|
if err then
|
|
sha256_digest = nil
|
|
return nil, err
|
|
end
|
|
|
|
_, err = sha256_digest:reset()
|
|
if err then
|
|
sha256_digest = nil
|
|
end
|
|
|
|
return output
|
|
end
|
|
|
|
---
|
|
-- Calculates SHA-256 hash of the value.
|
|
--
|
|
-- @function utils.sha256
|
|
-- @tparam string value value from which to calculate hash
|
|
-- @treturn string|nil sha-256 hash (32 bytes)
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local hash, err = require "resty.session.utils".sha256("hello world")
|
|
sha256 = function(value)
|
|
if not digest then
|
|
digest = require "resty.openssl.digest"
|
|
end
|
|
sha256 = sha256_real
|
|
return sha256(value)
|
|
end
|
|
end
|
|
|
|
|
|
local derive_hkdf_sha256 do
|
|
local kdf_derive
|
|
|
|
local EXTRACTED_KEYS = {}
|
|
|
|
local HKDF_SHA256_EXTRACT_OPTS
|
|
local HKDF_SHA256_EXPAND_OPTS
|
|
|
|
local function derive_hkdf_sha256_real(ikm, nonce, usage, size)
|
|
local err
|
|
local key = EXTRACTED_KEYS[ikm]
|
|
if not key then
|
|
HKDF_SHA256_EXTRACT_OPTS.hkdf_key = ikm
|
|
key, err = kdf_derive(HKDF_SHA256_EXTRACT_OPTS)
|
|
HKDF_SHA256_EXTRACT_OPTS.hkdf_key = ""
|
|
if not key then
|
|
return nil, err
|
|
end
|
|
EXTRACTED_KEYS[ikm] = key
|
|
end
|
|
|
|
HKDF_SHA256_EXPAND_OPTS.hkdf_key = key
|
|
HKDF_SHA256_EXPAND_OPTS.hkdf_info = usage .. ":" .. nonce
|
|
HKDF_SHA256_EXPAND_OPTS.outlen = size
|
|
key, err = kdf_derive(HKDF_SHA256_EXPAND_OPTS)
|
|
if not key then
|
|
return nil, err
|
|
end
|
|
|
|
return key
|
|
end
|
|
|
|
---
|
|
-- Derive a new key using HKDF with SHA-256.
|
|
--
|
|
-- @function utils.derive_hkdf_sha256
|
|
-- @tparam string ikm initial key material
|
|
-- @tparam string nonce nonce
|
|
-- @tparam string usage e.g. `"encryption"` or `"authentication"`
|
|
-- @tparam number size how many bytes to return
|
|
-- @treturn string|nil key material
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local ikm = utils.rand_bytes(32)
|
|
-- local nonce = utils.rand_bytes(32)
|
|
-- local key, err = utils.derive_hkdf_sha256(ikm, nonce, "encryption", 32)
|
|
derive_hkdf_sha256 = function(ikm, nonce, usage, size)
|
|
if not kdf_derive then
|
|
local kdf = require "resty.openssl.kdf"
|
|
HKDF_SHA256_EXTRACT_OPTS = {
|
|
type = kdf.HKDF,
|
|
outlen = 32,
|
|
md = "sha256",
|
|
salt = "",
|
|
hkdf_key = "",
|
|
hkdf_mode = kdf.HKDEF_MODE_EXTRACT_ONLY,
|
|
hkdf_info = "",
|
|
}
|
|
HKDF_SHA256_EXPAND_OPTS = {
|
|
type = kdf.HKDF,
|
|
outlen = 44,
|
|
md = "sha256",
|
|
salt = "",
|
|
hkdf_key = "",
|
|
hkdf_mode = kdf.HKDEF_MODE_EXPAND_ONLY,
|
|
hkdf_info = "",
|
|
}
|
|
kdf_derive = kdf.derive
|
|
end
|
|
derive_hkdf_sha256 = derive_hkdf_sha256_real
|
|
return derive_hkdf_sha256(ikm, nonce, usage, size)
|
|
end
|
|
end
|
|
|
|
|
|
local derive_pbkdf2_hmac_sha256 do
|
|
local kdf_derive
|
|
|
|
local PBKDF2_SHA256_OPTS
|
|
|
|
local function derive_pbkdf2_hmac_sha256_real(pass, salt, usage, size, iterations)
|
|
PBKDF2_SHA256_OPTS.pass = pass
|
|
PBKDF2_SHA256_OPTS.salt = usage .. ":" .. salt
|
|
PBKDF2_SHA256_OPTS.outlen = size
|
|
PBKDF2_SHA256_OPTS.pbkdf2_iter = iterations
|
|
local key, err = kdf_derive(PBKDF2_SHA256_OPTS)
|
|
if not key then
|
|
return nil, err
|
|
end
|
|
|
|
return key
|
|
end
|
|
|
|
---
|
|
-- Derive a new key using PBKDF2 with SHA-256.
|
|
--
|
|
-- @function utils.derive_pbkdf2_hmac_sha256
|
|
-- @tparam string pass password
|
|
-- @tparam string salt salt
|
|
-- @tparam string usage e.g. `"encryption"` or `"authentication"`
|
|
-- @tparam number size how many bytes to return
|
|
-- @tparam number iterations how many iterations to run, e.g. `10000`
|
|
-- @treturn string|nil key material
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local pass = "my-super-secret-password"
|
|
-- local salt = utils.rand_bytes(32)
|
|
-- local key, err = utils.derive_pbkdf2_hmac_sha256(pass, salt, "encryption", 32, 10000)
|
|
derive_pbkdf2_hmac_sha256 = function(pass, salt, usage, size, iterations)
|
|
if not kdf_derive then
|
|
local kdf = require "resty.openssl.kdf"
|
|
PBKDF2_SHA256_OPTS = {
|
|
type = kdf.PBKDF2,
|
|
outlen = 44,
|
|
md = "sha256",
|
|
pass = "",
|
|
salt = "",
|
|
pbkdf2_iter = 10000,
|
|
}
|
|
kdf_derive = kdf.derive
|
|
end
|
|
derive_pbkdf2_hmac_sha256 = derive_pbkdf2_hmac_sha256_real
|
|
return derive_pbkdf2_hmac_sha256(pass, salt, usage, size, iterations)
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Derive a new AES-256 GCM-mode key and initialization vector.
|
|
--
|
|
-- Safety can be:
|
|
-- * `nil` or `"None"`: key and iv will be derived using HKDF with SHA-256, except on FIPS-mode uses PBKDF2 with SHA-256 (single iteration)
|
|
-- * `Low`: key and iv will be derived using PBKDF2 with SHA-256 (1.000 iterations)
|
|
-- * `Medium`: key and iv will be derived using PBKDF2 with SHA-256 (10.000 iterations)
|
|
-- * `High`: key and iv will be derived using PBKDF2 with SHA-256 (100.000 iterations)
|
|
-- * `Very High`: key and iv will be derived using PBKDF2 with SHA-256 (1.000.000 iterations)
|
|
--
|
|
-- @function utils.derive_aes_gcm_256_key_and_iv
|
|
-- @tparam string ikm initial key material
|
|
-- @tparam string nonce nonce
|
|
-- @tparam[opt] string safety safety of key generation
|
|
-- @treturn string|nil key
|
|
-- @treturn string|nil error message
|
|
-- @treturn string|nil initialization vector
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local ikm = utils.rand_bytes(32)
|
|
-- local nonce = utils.rand_bytes(32)
|
|
-- local key, err, iv = utils.derive_aes_gcm_256_key_and_iv(ikm, nonce, "Medium")
|
|
local function derive_aes_gcm_256_key_and_iv(ikm, nonce, safety)
|
|
local bytes, err
|
|
if safety and safety ~= "None" then
|
|
if safety == "Very High" then
|
|
bytes, err = derive_pbkdf2_hmac_sha256(ikm, nonce, "encryption", 44, 1000000)
|
|
elseif safety == "High" then
|
|
bytes, err = derive_pbkdf2_hmac_sha256(ikm, nonce, "encryption", 44, 100000)
|
|
elseif safety == "Low" then
|
|
bytes, err = derive_pbkdf2_hmac_sha256(ikm, nonce, "encryption", 44, 1000)
|
|
else
|
|
bytes, err = derive_pbkdf2_hmac_sha256(ikm, nonce, "encryption", 44, 10000)
|
|
end
|
|
|
|
else
|
|
if is_fips_mode() then
|
|
bytes, err = derive_pbkdf2_hmac_sha256(ikm, nonce, "encryption", 44, 1)
|
|
else
|
|
bytes, err = derive_hkdf_sha256(ikm, nonce, "encryption", 44)
|
|
end
|
|
end
|
|
|
|
if not bytes then
|
|
return nil, err
|
|
end
|
|
|
|
local key = sub(bytes, 1, 32) -- 32 bytes
|
|
local iv = sub(bytes, 33, 44) -- 12 bytes
|
|
|
|
return key, nil, iv
|
|
end
|
|
|
|
|
|
---
|
|
-- Derive HMAC SHA-256 key for message authentication using HDKF with SHA-256,
|
|
-- except on FIPS-mode it uses PBKDF2 with SHA-256 (single iteration).
|
|
--
|
|
-- @function utils.derive_hmac_sha256_key
|
|
-- @tparam string ikm initial key material
|
|
-- @tparam string nonce nonce
|
|
-- @treturn string|nil key
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local ikm = utils.rand_bytes(32)
|
|
-- local nonce = utils.rand_bytes(32)
|
|
-- local key, err = utils.derive_hmac_sha256_key(ikm, nonce)
|
|
local function derive_hmac_sha256_key(ikm, nonce)
|
|
if is_fips_mode() then
|
|
return derive_pbkdf2_hmac_sha256(ikm, nonce, "authentication", 32, 1)
|
|
end
|
|
|
|
return derive_hkdf_sha256(ikm, nonce, "authentication", 32)
|
|
end
|
|
|
|
|
|
local encrypt_aes_256_gcm, decrypt_aes_256_gcm do
|
|
local AES_256_GCP_CIPHER = "aes-256-gcm"
|
|
local AES_256_GCM_TAG_SIZE = 16
|
|
|
|
local cipher_aes_256_gcm
|
|
local function encrypt_aes_256_gcm_real(key, iv, plaintext, aad)
|
|
local ciphertext, err = cipher_aes_256_gcm:encrypt(key, iv, plaintext, false, aad)
|
|
if not ciphertext then
|
|
return nil, err
|
|
end
|
|
|
|
local tag, err = cipher_aes_256_gcm:get_aead_tag(AES_256_GCM_TAG_SIZE)
|
|
if not tag then
|
|
return nil, err
|
|
end
|
|
|
|
return ciphertext, nil, tag
|
|
end
|
|
|
|
local function decrypt_aes_256_gcm_real(key, iv, ciphertext, aad, tag)
|
|
return cipher_aes_256_gcm:decrypt(key, iv, ciphertext, false, aad, tag)
|
|
end
|
|
|
|
---
|
|
-- Encrypt plain text using AES-256 in GCM-mode.
|
|
--
|
|
-- @function utils.encrypt_aes_256_gcm
|
|
-- @tparam string key encryption key
|
|
-- @tparam string iv initialization vector
|
|
-- @tparam string plaintext plain text
|
|
-- @tparam string aad additional authenticated data
|
|
-- @treturn string|nil ciphertext
|
|
-- @treturn string|nil error message
|
|
-- @treturn string|nil authentication tag
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local ikm = utils.rand_bytes(32)
|
|
-- local nonce = utils.rand_bytes(32)
|
|
-- local key, err, iv = utils.derive_aes_gcm_256_key_and_iv(ikm, nonce)
|
|
-- local enc, err, tag = utils.encrypt_aes_256_gcm(key, iv, "hello", "john@doe.com")
|
|
encrypt_aes_256_gcm = function(key, iv, plaintext, aad)
|
|
if not cipher_aes_256_gcm then
|
|
cipher_aes_256_gcm = require("resty.openssl.cipher").new(AES_256_GCP_CIPHER)
|
|
end
|
|
encrypt_aes_256_gcm = encrypt_aes_256_gcm_real
|
|
return encrypt_aes_256_gcm(key, iv, plaintext, aad)
|
|
end
|
|
|
|
---
|
|
-- Decrypt ciphertext using AES-256 in GCM-mode.
|
|
--
|
|
-- @function utils.decrypt_aes_256_gcm
|
|
-- @tparam string key encryption key
|
|
-- @tparam string iv initialization vector
|
|
-- @tparam string plaintext plain text
|
|
-- @tparam string aad additional authenticated data
|
|
-- @tparam string tag authentication tag
|
|
-- @treturn string|nil ciphertext
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local ikm = utils.rand_bytes(32)
|
|
-- local nonce = utils.rand_bytes(32)
|
|
-- local key, err, iv = utils.derive_aes_gcm_256_key_and_iv(ikm, nonce)
|
|
-- local enc, err, tag = utils.encrypt_aes_256_gcm(key, iv, "hello", "john@doe.com")
|
|
-- local out, err = utils.decrypt_aes_256_gcm(key, iv, ciphertext, "john@doe.com", tag)
|
|
decrypt_aes_256_gcm = function(key, iv, ciphertext, aad, tag)
|
|
if not cipher_aes_256_gcm then
|
|
cipher_aes_256_gcm = require("resty.openssl.cipher").new(AES_256_GCP_CIPHER)
|
|
end
|
|
decrypt_aes_256_gcm = decrypt_aes_256_gcm_real
|
|
return decrypt_aes_256_gcm(key, iv, ciphertext, aad, tag)
|
|
end
|
|
end
|
|
|
|
|
|
local hmac_sha256 do
|
|
local hmac
|
|
local HMAC_SHA256_DIGEST = "sha256"
|
|
|
|
local function hmac_sha256_real(key, value)
|
|
local mac, err = hmac.new(key, HMAC_SHA256_DIGEST)
|
|
if not mac then
|
|
return nil, err
|
|
end
|
|
|
|
local digest, err = mac:final(value)
|
|
if not digest then
|
|
return nil, err
|
|
end
|
|
|
|
return digest
|
|
end
|
|
|
|
---
|
|
-- Calculate message authentication code with HMAC with SHA-256.
|
|
--
|
|
-- @function utils.hmac_sha256
|
|
-- @tparam string key key
|
|
-- @tparam string value value
|
|
-- @treturn string|nil message authentication code (32 bytes)
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local ikm = utils.rand_bytes(32)
|
|
-- local nonce = utils.rand_bytes(32)
|
|
-- local key, err = utils.derive_hmac_sha256_key(ikm, nonce)
|
|
-- local mac, err = utils.hmac_sha256(key, "hello")
|
|
hmac_sha256 = function(key, value)
|
|
if not hmac then
|
|
hmac = require "resty.openssl.hmac"
|
|
end
|
|
hmac_sha256 = hmac_sha256_real
|
|
return hmac_sha256(key, value)
|
|
end
|
|
end
|
|
|
|
|
|
|
|
local load_storage do
|
|
local DSHM
|
|
local FILE
|
|
local MEMCACHED
|
|
local MYSQL
|
|
local POSTGRES
|
|
local REDIS
|
|
local REDIS_SENTINEL
|
|
local REDIS_CLUSTER
|
|
local SHM
|
|
local CUSTOM = {}
|
|
|
|
---
|
|
-- Loads session storage and creates a new instance using session configuration.
|
|
--
|
|
-- @function utils.load_storage
|
|
-- @tparam string storage name of the storage to load
|
|
-- @tparam[opt] table configuration session configuration
|
|
-- @treturn table|nil instance of session storage
|
|
-- @treturn string|nil error message
|
|
--
|
|
-- @usage
|
|
-- local postgres = require "resty.session.utils".load_storage("postgres", {
|
|
-- postgres = {
|
|
-- host = "127.0.0.1",
|
|
-- }
|
|
-- })
|
|
load_storage = function(storage, configuration)
|
|
if storage == "cookie" then
|
|
return nil
|
|
|
|
elseif storage == "dshm" then
|
|
if not DSHM then
|
|
DSHM = require("resty.session.dshm")
|
|
end
|
|
return DSHM.new(configuration and configuration.dshm)
|
|
|
|
elseif storage == "file" then
|
|
if not FILE then
|
|
FILE = require("resty.session.file")
|
|
end
|
|
return FILE.new(configuration and configuration.file)
|
|
|
|
elseif storage == "memcached" or storage == "memcache" then
|
|
if not MEMCACHED then
|
|
MEMCACHED = require("resty.session.memcached")
|
|
end
|
|
return MEMCACHED.new(configuration and configuration.memcached or configuration.memcache)
|
|
|
|
elseif storage == "mysql" or storage == "mariadb" then
|
|
if not MYSQL then
|
|
MYSQL = require("resty.session.mysql")
|
|
end
|
|
return MYSQL.new(configuration and configuration.mysql or configuration.mariadb)
|
|
|
|
elseif storage == "postgres" or storage == "postgresql" then
|
|
if not POSTGRES then
|
|
POSTGRES = require("resty.session.postgres")
|
|
end
|
|
return POSTGRES.new(configuration and configuration.postgres or configuration.postgresql)
|
|
|
|
elseif storage == "redis" then
|
|
local cfg = configuration and configuration.redis
|
|
if cfg then
|
|
if cfg.nodes then
|
|
if not REDIS_CLUSTER then
|
|
REDIS_CLUSTER = require("resty.session.redis.cluster")
|
|
end
|
|
return REDIS_CLUSTER.new(cfg)
|
|
|
|
elseif cfg.sentinels then
|
|
if not REDIS_SENTINEL then
|
|
REDIS_SENTINEL = require("resty.session.redis.sentinel")
|
|
end
|
|
return REDIS_SENTINEL.new(cfg)
|
|
end
|
|
end
|
|
|
|
if not REDIS then
|
|
REDIS = require("resty.session.redis")
|
|
end
|
|
return REDIS.new(cfg)
|
|
|
|
elseif storage == "shm" then
|
|
if not SHM then
|
|
SHM = require("resty.session.shm")
|
|
end
|
|
|
|
return SHM.new(configuration and configuration.shm)
|
|
|
|
else
|
|
if not CUSTOM[storage] then
|
|
CUSTOM[storage] = require(storage)
|
|
end
|
|
return CUSTOM[storage].new(configuration and configuration[storage])
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Helper to format error messages.
|
|
--
|
|
-- @function utils.errmsg
|
|
-- @tparam[opt] string|nil err a possible error coming from underlying library
|
|
-- @tparam string|nil msg error message
|
|
-- @tparam any ... arguments for formatting the msg
|
|
-- @treturn string formatted error message
|
|
--
|
|
-- @usage
|
|
-- local utils = require "resty.session.utils"
|
|
-- local test = "aaaa"
|
|
-- local data, err = utils.deflate(test)
|
|
-- if not data then
|
|
-- print(utils.errmsg(err, "unable to deflate data '%s'", test)
|
|
-- end
|
|
local function errmsg(err, msg, ...)
|
|
if not msg then
|
|
if err then
|
|
return err
|
|
end
|
|
|
|
return "unknown error"
|
|
end
|
|
|
|
if select("#", ...) > 0 then
|
|
msg = fmt(msg, ...)
|
|
end
|
|
|
|
if err then
|
|
return fmt("%s (%s)", msg, err)
|
|
end
|
|
|
|
return msg
|
|
end
|
|
|
|
|
|
---
|
|
-- Helper to create a delimited key.
|
|
--
|
|
-- @function utils.get_name
|
|
-- @tparam table storage a storage implementation
|
|
-- @tparam string name name
|
|
-- @tparam string key key
|
|
-- @tparam string subject subject
|
|
-- @treturn string formatted and delimited name
|
|
local function get_name(storage, name, key, subject)
|
|
local prefix = storage.prefix
|
|
local suffix = storage.suffix
|
|
if prefix and suffix and subject then
|
|
return fmt("%s:%s:%s:%s:%s", prefix, name, key, subject, suffix)
|
|
elseif prefix and suffix then
|
|
return fmt("%s:%s:%s:%s", prefix, name, key, suffix)
|
|
elseif prefix and subject then
|
|
return fmt("%s:%s:%s:%s", prefix, name, key, subject)
|
|
elseif suffix and subject then
|
|
return fmt("%s:%s:%s:%s", name, key, subject, suffix)
|
|
elseif prefix then
|
|
return fmt("%s:%s:%s", prefix, name, key)
|
|
elseif suffix then
|
|
return fmt("%s:%s:%s", name, key, suffix)
|
|
elseif subject then
|
|
return fmt("%s:%s:%s", name, key, subject)
|
|
else
|
|
return fmt("%s:%s", name, key)
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Helper to turn on a flag.
|
|
--
|
|
-- @function utils.set_flag
|
|
-- @tparam number flags flags on which the flag is applied
|
|
-- @tparam number flag flag that is applied
|
|
-- @treturn number flags with the flag applied
|
|
--
|
|
-- @usage
|
|
-- local flags = 0x0000
|
|
-- local FLAG_DOG = 0x001
|
|
-- flags = utils.set_flag(flags, FLAG_DOG)
|
|
local function set_flag(options, flag)
|
|
return bor(options, flag)
|
|
end
|
|
|
|
|
|
---
|
|
-- Helper to turn off a flag.
|
|
--
|
|
-- @function utils.unset_flag
|
|
-- @tparam number flags flags on which the flag is removed
|
|
-- @tparam number flag flag that is removed
|
|
-- @treturn number flags with the flag removed
|
|
--
|
|
-- @usage
|
|
-- local options = 0x0000
|
|
-- local FLAG_DOG = 0x001
|
|
-- flags = utils.set_flag(options, FLAG_DOG)
|
|
-- flags = utils.unset_flag(options, FLAG_DOG)
|
|
local function unset_flag(flags, flag)
|
|
return band(flags, bnot(flag))
|
|
end
|
|
|
|
|
|
---
|
|
-- Helper to check if flag is enabled.
|
|
--
|
|
-- @function utils.has_flag
|
|
-- @tparam number flags flags on which the flag is checked
|
|
-- @tparam number flag flag that is checked
|
|
-- @treturn boolean true if flag has is present, otherwise false
|
|
--
|
|
-- @usage
|
|
-- local flags = 0x0000
|
|
-- local FLAG_DOG = 0x001
|
|
-- local FLAG_BONE = 0x010
|
|
-- flags = utils.set_flag(flags, FLAG_DOG)
|
|
-- flags = utils.set_flag(flags, FLAG_BONE)
|
|
-- print(utils.has_flag(flags, FLAG_BONE)
|
|
local function has_flag(flags, flag)
|
|
return band(flags, flag) ~= 0
|
|
end
|
|
|
|
|
|
---
|
|
-- Helper to get the value used to store metadata for a certain aud and sub
|
|
-- Empty exp means the session id has been invalidated
|
|
--
|
|
-- @function utils.meta_get_value
|
|
-- @tparam string key storage key
|
|
-- @tparam string exp expiration
|
|
-- @treturn string the value to store in the metadata collection
|
|
local function meta_get_value(key, exp)
|
|
if not exp or exp == 0 then
|
|
return fmt("%s;", key)
|
|
end
|
|
return fmt("%s:%s;", key, encode_base64url(bpack(5, exp)))
|
|
end
|
|
|
|
|
|
local meta_get_next do
|
|
local KEY_OFFSET = 43
|
|
local DEL_OFFSET = 1
|
|
local EXP_OFFSET = 7
|
|
|
|
local COLON = byte(":")
|
|
|
|
---
|
|
-- Function to extract the next key and exp from a serialized
|
|
-- metadata list, starting from index
|
|
--
|
|
-- @function utils.meta_get_next
|
|
-- @tparam string val list of key:exp;
|
|
-- @tparam number index start index
|
|
-- @treturn key string session id
|
|
-- @treturn err string error
|
|
-- @treturn exp number expiration
|
|
-- @treturn index number|nil index of the cursor
|
|
meta_get_next = function(val, index)
|
|
local key = sub(val, index, index + KEY_OFFSET - 1)
|
|
index = index + KEY_OFFSET
|
|
local del = byte(val, index + DEL_OFFSET - 1)
|
|
index = index + DEL_OFFSET
|
|
|
|
if del ~= COLON then
|
|
return key, nil, nil, index
|
|
end
|
|
|
|
local exp = sub(val, index, index + EXP_OFFSET - 1)
|
|
index = index + EXP_OFFSET + DEL_OFFSET
|
|
local exp, err = decode_base64url(exp)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
exp = bunpack(5, exp)
|
|
return key, nil, exp, index
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Function to filter out the latest valid key:exp from a
|
|
-- serialized list, used to store session metadata
|
|
--
|
|
-- @function utils.meta_get_latest
|
|
-- @tparam string sessions list of key:exp;
|
|
-- @treturn table valid sessions and their exp
|
|
local function meta_get_latest(sessions, current_time)
|
|
current_time = current_time
|
|
|
|
local latest = {}
|
|
local index = 1
|
|
local length = #sessions
|
|
while index < length do
|
|
local key, err, exp
|
|
key, err, exp, index = meta_get_next(sessions, index)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
if exp and exp > current_time then
|
|
latest[key] = exp
|
|
else
|
|
latest[key] = nil
|
|
end
|
|
end
|
|
|
|
return latest
|
|
end
|
|
|
|
|
|
return {
|
|
is_fips_mode = is_fips_mode,
|
|
bpack = bpack,
|
|
bunpack = bunpack,
|
|
trim = trim,
|
|
encode_json = encode_json,
|
|
decode_json = decode_json,
|
|
encode_base64url = encode_base64url,
|
|
decode_base64url = decode_base64url,
|
|
base64_size = base64_size,
|
|
inflate = inflate,
|
|
deflate = deflate,
|
|
rand_bytes = rand_bytes,
|
|
sha256 = sha256,
|
|
derive_hkdf_sha256 = derive_hkdf_sha256,
|
|
derive_pbkdf2_hmac_sha256 = derive_pbkdf2_hmac_sha256,
|
|
derive_aes_gcm_256_key_and_iv = derive_aes_gcm_256_key_and_iv,
|
|
derive_hmac_sha256_key = derive_hmac_sha256_key,
|
|
encrypt_aes_256_gcm = encrypt_aes_256_gcm,
|
|
decrypt_aes_256_gcm = decrypt_aes_256_gcm,
|
|
hmac_sha256 = hmac_sha256,
|
|
load_storage = load_storage,
|
|
errmsg = errmsg,
|
|
get_name = get_name,
|
|
set_flag = set_flag,
|
|
unset_flag = unset_flag,
|
|
has_flag = has_flag,
|
|
meta_get_value = meta_get_value,
|
|
meta_get_next = meta_get_next,
|
|
meta_get_latest = meta_get_latest,
|
|
}
|