aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--util/paseto.lua123
1 files changed, 123 insertions, 0 deletions
diff --git a/util/paseto.lua b/util/paseto.lua
new file mode 100644
index 00000000..5f162ad0
--- /dev/null
+++ b/util/paseto.lua
@@ -0,0 +1,123 @@
+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;
+};