local logic = require("bit") local json = require("cjson.safe") local aes = require("resty.openssl.cipher") local random = require("resty.random") local s256 = require("resty.sha256") local b64 = require("ssso_base64") local config = require("ssso_config") local log = require("ssso_log") local nginx = require("ssso_nginx") local sites = require("ssso_sites") local KEY_SIZE = 32 -- 256 bits for AES-256-GCM’s key and SHA-256 local IV_SIZE = 12 -- 96 bits for AES-256-GCM’s IV local TAG_SIZE = 16 -- 128 bits for AES-256-GCM’s tag local gcm_aad = random.bytes(8) -- https://www.rfc-editor.org/rfc/rfc7518.html#section-6.4 local symkey = random.bytes(KEY_SIZE, true) or random.bytes(KEY_SIZE, false) local keytype = '{"kty":"oct","k":"' .. b64.encode_base64url(symkey) .. '"}' -- https://en.wikipedia.org/wiki/HMAC local i_key_pad = "" local o_key_pad = "" for c in symkey:gmatch(".") do i_key_pad = i_key_pad .. string.char(logic.bxor(54, c:byte())) o_key_pad = o_key_pad .. string.char(logic.bxor(92, c:byte())) end -- https://www.rfc-editor.org/rfc/rfc7515.html#appendix-A.1 local jose_256_b64 = b64.encode_base64url('{"alg":"HS256"}') local function encrypt(bytes) local iv = random.bytes(IV_SIZE, true) or random.bytes(IV_SIZE, false) local gcm = aes.new("aes-256-gcm") local crypted = gcm:encrypt(symkey, iv, bytes, false, gcm_aad) if not crypted then return nil end local tag = gcm:get_aead_tag() return iv .. crypted .. tag end local function decrypt(bytes) local iv = bytes:sub(1, IV_SIZE) local contents = bytes:sub(IV_SIZE + 1, -TAG_SIZE - 1) local tag = bytes:sub(-TAG_SIZE) local gcm = aes.new("aes-256-gcm") local decrypted = gcm:decrypt(symkey, iv, contents, false, gcm_aad, tag) return decrypted end local function hmac(message) local inner = s256:new() inner:update(i_key_pad .. message) local outer = s256:new() outer:update(o_key_pad .. inner:final()) return outer:final() end local function to_jws(jwt) local jwt64 = b64.encode_base64url(json.encode(jwt)) return jose_256_b64 .. "." .. jwt64 .. "." .. b64.encode_base64url(hmac(jose_256_b64 .. jwt64)) end local function to_jwt(jws) local jwslen = #jws local dot1, _ = jws:find("%.") if not dot1 or dot1 == jwslen then return nil end local dot2, _ = jws:find("%.", dot1 + 1) if not dot2 or dot2 == jwslen then return nil end local jose64 = jws:sub(1, dot1 - 1) if jose64 ~= jose_256_b64 then return nil end local js64 = jws:sub(dot1 + 1, dot2 - 1) local sig = jws:sub(dot2 + 1) if sig ~= b64.encode_base64url(hmac(jose64 .. js64)) then return nil end return json.decode(b64.decode_base64url(js64)) end -- https://www.rfc-editor.org/rfc/rfc7519 -- https://openid.net/specs/openid-connect-core-1_0.html local function get_jws_and_tslimit(profile) local user = profile:user() local ser_profile = profile:serialize() log.debug("Creating JWS with profile: " .. ser_profile:gsub("([\031\030\029\028\027\026])", function(s) return "[" .. s:byte() .. "]" end)) local crypted_profile = encrypt(ser_profile) if not user or not crypted_profile then return nil, nil end local iat = nginx.get_seconds_since_epoch() local exp = iat + config.get_session_seconds() local jwt = { iss = "https://" .. config.get_sso_host(), sub = user, aud = user, exp = exp, iat = iat, x_ssso = b64.encode_base64url(crypted_profile), } return to_jws(jwt), exp end local function get_profile_and_new_jws(jws) local jwt = to_jwt(jws) local iat = nginx.get_seconds_since_epoch() if jwt == nil or not jwt["x_ssso"] or not jwt["exp"] or jwt.exp < iat then return nil, nil, nil end local ser_profile = decrypt(b64.decode_base64url(jwt.x_ssso)) if not ser_profile then return nil, nil, nil end log.debug("Read profile from JWS: " .. ser_profile:gsub("([\031\030\029\028])", function(s) return "[" .. s:byte() .. "]" end)) local profile = sites.class__profile:deserialize(ser_profile) jwt.iat = iat jwt.exp = iat + config.get_session_seconds() return profile, to_jws(jwt), jwt.exp end return { get_jws_and_tslimit = get_jws_and_tslimit, get_profile_and_new_jws = get_profile_and_new_jws, }