mirror of
https://github.com/bjc/prosody.git
synced 2025-04-01 20:27:39 +03:00
218 lines
6.5 KiB
Lua
218 lines
6.5 KiB
Lua
local crypto = require "prosody.util.crypto";
|
|
local json = require "prosody.util.json";
|
|
local hashes = require "prosody.util.hashes";
|
|
local base64_encode = require "prosody.util.encodings".base64.encode;
|
|
local base64_decode = require "prosody.util.encodings".base64.decode;
|
|
local secure_equals = require "prosody.util.hashes".equals;
|
|
local bit = require "prosody.util.bitcompat";
|
|
local hex = require "prosody.util.hex";
|
|
local rand = require "prosody.util.random";
|
|
local s_pack = require "prosody.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;
|
|
};
|