File

util/paseto.lua @ 12705:008a7097fdc5

util.jwt: Provide built-in token expiry support (defaults to 3600s lifetime) To avoid every user of the library needing to add and verify expiry info, this is now handled by util.jwt itself (if not overridden or disabled). Issuing tokens that are valid forever is bad practice and rarely desired, and the default token lifetime is now 3600s (1 hour).
author Matthew Wild <mwild1@gmail.com>
date Mon, 11 Jul 2022 13:28:29 +0100
parent 12694:26a004c96ef8
child 12709:b3f7c77c1f08
line wrap: on
line source

local crypto = require "util.crypto";
local json = require "util.json";
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 s_pack = require "util.struct".pack;

local s_gsub = string.gsub;

local pubkey_methods = {};
local privkey_methods = {};

local v4_public_pubkey_mt = { __index = pubkey_methods };
local v4_public_privkey_mt = { __index = privkey_methods };
local v4_public = {};

local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" };
local function b64url(data)
	return (s_gsub(base64_encode(data), "[+/=]", b64url_rep));
end
local function unb64url(data)
	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)
	local o = { le64(#parts) };
	for _, part in ipairs(parts) do
		table.insert(o, le64(#part)..part);
	end
	return table.concat(o);
end

function privkey_methods:export()
	return self.key:private_pem();
end

function pubkey_methods:export()
	return self.key:public_pem();
end

function v4_public.sign(m, sk, f, i)
	if getmetatable(sk) ~= v4_public_privkey_mt then
		error("cannot sign v4.public tokens with this key");
	end
	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.key, 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)
	if getmetatable(pk) ~= v4_public_pubkey_mt then
		error("cannot verify v4.public tokens with this key");
	end
	local h, sm, f = tok:match("^(v4%.public%.)([^%.]+)%.?(.*)$");
	if not h then
		return nil, "invalid-token-format";
	end
	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.key, 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

function v4_public.new_keypair()
	local key = crypto.generate_ed25519_keypair();
	return {
		private_key = setmetatable({
			key = key;
		}, v4_public_privkey_mt);
		public_key = setmetatable({
			key = key;
		}, v4_public_pubkey_mt);
	};
end

function v4_public.import_public_key(pem)
	local key = crypto.import_public_pem(pem);
	return setmetatable({
		key = key;
	}, v4_public_pubkey_mt);
end

function v4_public.import_private_key(pem)
	local key = crypto.import_private_pem(pem);
	return setmetatable({
		key = key;
	}, v4_public_privkey_mt);
end

return {
	pae = pae;
	v4_public = v4_public;
};