Software /
code /
prosody
File
util/paseto.lua @ 12895:4968dce04f4b
Merge 0.12->trunk
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Mon, 20 Feb 2023 18:10:36 +0000 |
parent | 12840:33d902b093f0 |
child | 12975:d10957394a3c |
line wrap: on
line source
local crypto = require "util.crypto"; local json = require "util.json"; local hashes = require "util.hashes"; local base64_encode = require "util.encodings".base64.encode; local base64_decode = require "util.encodings".base64.decode; local secure_equals = require "util.hashes".equals; local bit = require "util.bitcompat"; local hex = require "util.hex"; local rand = require "util.random"; local s_pack = require "util.struct".pack; local s_gsub = string.gsub; local v4_public = {}; local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" }; local function b64url(data) return (s_gsub(base64_encode(data), "[+/=]", b64url_rep)); end local valid_tails = { nil; -- Always invalid "^.[AQgw]$"; -- b??????00 "^..[AQgwEUk0IYo4Mcs8]$"; -- b????0000 } local function unb64url(data) local rem = #data%4; if data:sub(-1,-1) == "=" or rem == 1 or (rem > 1 and not data:sub(-rem):match(valid_tails[rem])) then return nil; end return base64_decode(s_gsub(data, "[-_]", b64url_rep).."=="); end local function le64(n) return s_pack("<I8", bit.band(n, 0x7F)); end local function pae(parts) if type(parts) ~= "table" then error("bad argument #1 to 'pae' (table expected, got "..type(parts)..")"); end local o = { le64(#parts) }; for _, part in ipairs(parts) do table.insert(o, le64(#part)..part); end return table.concat(o); end function v4_public.sign(m, sk, f, i) if type(m) ~= "table" then return nil, "PASETO payloads must be a table"; end m = json.encode(m); local h = "v4.public."; local m2 = pae({ h, m, f or "", i or "" }); local sig = crypto.ed25519_sign(sk, m2); if not f or f == "" then return h..b64url(m..sig); else return h..b64url(m..sig).."."..b64url(f); end end function v4_public.verify(tok, pk, expected_f, i) local h, sm, f = tok:match("^(v4%.public%.)([^%.]+)%.?(.*)$"); if not h then return nil, "invalid-token-format"; end f = f and unb64url(f) or nil; if expected_f then if not f or not secure_equals(expected_f, f) then return nil, "invalid-footer"; end end local raw_sm = unb64url(sm); if not raw_sm or #raw_sm <= 64 then return nil, "invalid-token-format"; end local s, m = raw_sm:sub(-64), raw_sm:sub(1, -65); local m2 = pae({ h, m, f or "", i or "" }); local ok = crypto.ed25519_verify(pk, m2, s); if not ok then return nil, "invalid-token"; end local payload, err = json.decode(m); if err ~= nil or type(payload) ~= "table" then return nil, "json-decode-error"; end return payload; end v4_public.import_private_key = crypto.import_private_pem; v4_public.import_public_key = crypto.import_public_pem; function v4_public.new_keypair() return crypto.generate_ed25519_keypair(); end function v4_public.init(private_key_pem, public_key_pem, options) local sign, verify = v4_public.sign, v4_public.verify; local public_key = public_key_pem and v4_public.import_public_key(public_key_pem); local private_key = private_key_pem and v4_public.import_private_key(private_key_pem); local default_footer = options and options.default_footer; local default_assertion = options and options.default_implicit_assertion; return private_key and function (token, token_footer, token_assertion) return sign(token, private_key, token_footer or default_footer, token_assertion or default_assertion); end, public_key and function (token, expected_footer, token_assertion) return verify(token, public_key, expected_footer or default_footer, token_assertion or default_assertion); end; end function v4_public.new_signer(private_key_pem, options) return (v4_public.init(private_key_pem, nil, options)); end function v4_public.new_verifier(public_key_pem, options) return (select(2, v4_public.init(nil, public_key_pem, options))); end local v3_local = { _key_mt = {} }; local function v3_local_derive_keys(k, n) local tmp = hashes.hkdf_hmac_sha384(48, k, nil, "paseto-encryption-key"..n); local Ek = tmp:sub(1, 32); local n2 = tmp:sub(33); local Ak = hashes.hkdf_hmac_sha384(48, k, nil, "paseto-auth-key-for-aead"..n); return Ek, Ak, n2; end function v3_local.encrypt(m, k, f, i) assert(#k == 32) if type(m) ~= "table" then return nil, "PASETO payloads must be a table"; end m = json.encode(m); local h = "v3.local."; local n = rand.bytes(32); local Ek, Ak, n2 = v3_local_derive_keys(k, n); local c = crypto.aes_256_ctr_encrypt(Ek, n2, m); local m2 = pae({ h, n, c, f or "", i or "" }); local t = hashes.hmac_sha384(Ak, m2); if not f or f == "" then return h..b64url(n..c..t); else return h..b64url(n..c..t).."."..b64url(f); end end function v3_local.decrypt(tok, k, expected_f, i) assert(#k == 32) local h, sm, f = tok:match("^(v3%.local%.)([^%.]+)%.?(.*)$"); if not h then return nil, "invalid-token-format"; end f = f and unb64url(f) or nil; if expected_f then if not f or not secure_equals(expected_f, f) then return nil, "invalid-footer"; end end local m = unb64url(sm); if not m or #m <= 80 then return nil, "invalid-token-format"; end local n, c, t = m:sub(1, 32), m:sub(33, -49), m:sub(-48); local Ek, Ak, n2 = v3_local_derive_keys(k, n); local preAuth = pae({ h, n, c, f or "", i or "" }); local t2 = hashes.hmac_sha384(Ak, preAuth); if not secure_equals(t, t2) then return nil, "invalid-token"; end local m2 = crypto.aes_256_ctr_decrypt(Ek, n2, c); if not m2 then return nil, "invalid-token"; end local payload, err = json.decode(m2); if err ~= nil or type(payload) ~= "table" then return nil, "json-decode-error"; end return payload; end function v3_local.new_key() return "secret-token:paseto.v3.local:"..hex.encode(rand.bytes(32)); end function v3_local.init(key, options) local encoded_key = key:match("^secret%-token:paseto%.v3%.local:(%x+)$"); if not encoded_key or #encoded_key ~= 64 then return error("invalid key for v3.local"); end local raw_key = hex.decode(encoded_key); local default_footer = options and options.default_footer; local default_assertion = options and options.default_implicit_assertion; return function (token, token_footer, token_assertion) return v3_local.encrypt(token, raw_key, token_footer or default_footer, token_assertion or default_assertion); end, function (token, token_footer, token_assertion) return v3_local.decrypt(token, raw_key, token_footer or default_footer, token_assertion or default_assertion); end; end function v3_local.new_signer(key, options) return (v3_local.init(key, options)); end function v3_local.new_verifier(key, options) return (select(2, v3_local.init(key, options))); end return { pae = pae; v3_local = v3_local; v4_public = v4_public; };