Skip to content

Commit d4d9db7

Browse files
authored
Merge pull request #2 from peter-evans/0.2.0
0.2.0
2 parents 8a38f8a + 5b26ccc commit d4d9db7

9 files changed

Lines changed: 1022 additions & 319 deletions

File tree

.travis.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ language: python
22
sudo: true
33

44
env:
5-
- LUA="lua=5.1"
6-
- LUA="lua=5.2"
7-
- LUA="lua=5.3"
8-
- LUA="luajit=2.0"
9-
- LUA="luajit=2.1"
5+
- LUA="lua=5.1" COMPAT="default" CJSON_VERSION="2.1.0"
6+
- LUA="lua=5.2" COMPAT="default" CJSON_VERSION="2.1.0"
7+
- LUA="lua=5.3" COMPAT="all" CJSON_VERSION="2.1.0"
8+
- LUA="luajit=2.0" COMPAT="default" CJSON_VERSION="2.1.0"
9+
- LUA="luajit=2.1" COMPAT="default" CJSON_VERSION="2.1.0.6"
1010

1111
before_install:
1212
- pip install hererocks
13-
- hererocks lua_install -r^ --$LUA
13+
- hererocks lua_install -r^ --$LUA --compat=$COMPAT
1414
- export PATH=$PATH:$PWD/lua_install/bin
1515

1616
install:
@@ -19,6 +19,7 @@ install:
1919
- luarocks install busted
2020
- luarocks install luacov
2121
- luarocks install luacov-coveralls
22+
- luarocks install lua-cjson $CJSON_VERSION
2223
- luarocks make
2324

2425
script:

README.md

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,42 @@ This implementation doesn't support the v1 protocol. Please note that v1 should
2020
#### Roadmap
2121
- [x] v2 local authentication (Blake2b and XChaCha20-Poly1305)
2222
- [x] v2 public authentication (Ed25519 signatures)
23-
- [ ] Payload processing and registered claims validation
24-
- [ ] Key-ID support
25-
- [ ] High-level token creation API
23+
- [x] JSON payload and footer processing
24+
- [ ] Claims validation
25+
- [ ] Integrated key ID (kid) support
26+
- [ ] High-level token builder API
2627
- [ ] API documentation
2728

2829
## Installation
2930

31+
#### Sodium Crypto Library
32+
3033
This Lua module depends on the [Sodium crypto library (libsodium)](https://github.com/jedisct1/libsodium).
3134
The following is a convenient way to install libsodium via LuaRocks.
3235
I don't necessarily recommend this for production use. Please see [libsodium's documentation](https://download.libsodium.org/doc/installation/) for full installation instructions.
3336
```
3437
luarocks install libsodium
3538
```
3639

37-
Then install PASETO:
40+
#### Lua CJSON
41+
42+
This module also has a dependency on [lua-cjson](https://luarocks.org/modules/openresty/lua-cjson).
43+
Due to an issue with the latest version you might need to specify an earlier stable version.
44+
If you let luarocks install this dependency for you when installing PASETO you might experience problems.
45+
Additionally, if using Lua 5.3 you need to build Lua with a compatibility flag for Lua 5.1.
46+
47+
| Lua | Lua CJSON install | Lua compatibility flags |
48+
| :---: | :---: | :---: |
49+
| 5.1 | `luarocks install lua-cjson 2.1.0` | |
50+
| 5.2 | `luarocks install lua-cjson 2.1.0` | |
51+
| 5.3 | `luarocks install lua-cjson 2.1.0` | -DLUA_COMPAT_5_1 |
52+
| Lua JIT 2.0 | `luarocks install lua-cjson 2.1.0` | |
53+
| Lua JIT 2.1 | `luarocks install lua-cjson 2.1.0.6` | |
54+
55+
See [.travis.yml](.travis.yml) for build configurations.
56+
57+
#### PASETO
58+
Finally, install PASETO:
3859
```
3960
luarocks install paseto
4061
```
@@ -45,7 +66,58 @@ __v2.local__ :
4566
```
4667
local paseto = require "paseto.v2"
4768
48-
local key, message, token, footer, decrypted
69+
local key, payload_claims, token, footer_claims
70+
local extracted_footer_claims, extracted_footer, decrypted_claims
71+
payload_claims = {}
72+
payload_claims["clientid"] = 100099
73+
payload_claims["message"] = "secret"
74+
footer_claims = { kid = "123456789" }
75+
76+
-- generate symmetric key
77+
key = paseto.generate_symmetric_key()
78+
79+
-- encrypt/decrypt without footer
80+
token = paseto.encrypt(key, payload_claims)
81+
decrypted_claims = paseto.decrypt(key, token)
82+
83+
-- encrypt/decrypt with footer
84+
token = paseto.encrypt(key, payload_claims, footer_claims)
85+
extracted_footer_claims, extracted_footer = paseto.extract_footer_claims(token)
86+
decrypted_claims = paseto.decrypt(key, token, extracted_footer)
87+
```
88+
89+
__v2.public__ :
90+
```
91+
local paseto = require "paseto.v2"
92+
93+
local secret_key, public_key, payload_claims, token, footer_claims
94+
local extracted_footer_claims, extracted_footer, verified_claims
95+
payload_claims = {}
96+
payload_claims["clientid"] = 100099
97+
payload_claims["message"] = "secret"
98+
footer_claims = { kid = "123456789" }
99+
100+
-- generate key pair
101+
secret_key, public_key = paseto.generate_asymmetric_secret_key()
102+
103+
-- sign/verify without footer
104+
token = paseto.sign(secret_key, payload_claims)
105+
verified_claims = paseto.verify(public_key, token)
106+
107+
-- sign/verify with footer
108+
token = paseto.sign(secret_key, payload_claims, footer_claims)
109+
extracted_footer_claims, extracted_footer = paseto.extract_footer_claims(token)
110+
verified_claims = paseto.verify(public_key, token, extracted_footer)
111+
```
112+
113+
#### Core API
114+
This low-level API should only be used when you cannot use JSON for payload claims and footer claims.
115+
116+
__v2.local__ :
117+
```
118+
local paseto = require "paseto.v2.core"
119+
120+
local key, message, token, footer, extracted_footer, decrypted
49121
message = "my secret message"
50122
footer = "my footer"
51123
@@ -58,14 +130,15 @@ decrypted = paseto.decrypt(key, token)
58130
59131
-- encrypt/decrypt with footer
60132
token = paseto.encrypt(key, message, footer)
61-
decrypted = paseto.decrypt(key, token, footer)
133+
extracted_footer = paseto.extract_footer(token)
134+
decrypted = paseto.decrypt(key, token, extracted_footer)
62135
```
63136

64137
__v2.public__ :
65138
```
66-
local paseto = require "paseto.v2"
139+
local paseto = require "paseto.v2.core"
67140
68-
local secret_key, public_key, message, token, footer, verified
141+
local secret_key, public_key, message, token, footer, extracted_footer, verified
69142
message = "my secret message"
70143
footer = "my footer"
71144
@@ -78,7 +151,8 @@ verified = paseto.verify(public_key, token)
78151
79152
-- sign/verify with footer
80153
token = paseto.sign(secret_key, message, footer)
81-
verified = paseto.verify(public_key, token, footer)
154+
extracted_footer = paseto.extract_footer(token)
155+
verified = paseto.verify(public_key, token, extracted_footer)
82156
```
83157

84158
## License

local/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
FROM python:3.6.5
22

33
ENV LUA_VERSION 5.3
4+
ENV COMPAT all
45
ENV LUA_DIR "/lua_install"
56
ENV PATH "$PATH:$LUA_DIR/bin"
67

78
RUN apt-get -y update \
89
&& apt-get install -y -qq --no-install-recommends sudo zip unzip \
910
&& pip install hererocks \
10-
&& hererocks $LUA_DIR -r^ --lua=$LUA_VERSION \
11+
&& hererocks $LUA_DIR -r^ --lua=$LUA_VERSION --compat=$COMPAT \
1112
&& luarocks install libsodium \
1213
&& luarocks install basexx \
1314
&& luarocks install lua-struct \
15+
&& luarocks install lua-cjson 2.1.0 \
1416
&& luarocks install busted \
1517
&& luarocks install luacheck \
1618
&& luarocks install luacov

paseto/v2.lua

Lines changed: 84 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
local v2 = {
2-
_VERSION = 'paseto v0.1.0',
2+
_VERSION = 'paseto v0.2.0',
33
_DESCRIPTION = 'PASETO (Platform-Agnostic Security Tokens) for Lua',
44
_URL = 'https://github.com/peter-evans/paseto-lua',
55
_LICENSE = [[
@@ -27,121 +27,121 @@ local v2 = {
2727
]]
2828
}
2929

30-
local PROTOCOL_VERSION = "v2"
31-
local luasodium = require("luasodium")
32-
local basexx = require("basexx")
33-
local struct = require("struct")
30+
local paseto = require "paseto.v2.core"
31+
local json = require "cjson"
3432

35-
function v2.__pre_auth_encode(...)
36-
local encoded = struct.pack("<L", #{...})
37-
for _, piece in ipairs({...})
38-
do
39-
encoded = encoded .. struct.pack("<L", #piece) .. piece
33+
local function encode_json(data)
34+
local ok, encoded = pcall(function()
35+
return json.encode(data)
36+
end)
37+
if not ok then
38+
return nil, "Invalid data"
4039
end
4140
return encoded
4241
end
4342

44-
function v2.__validate_and_remove_footer(token, footer)
45-
if not footer or footer == "" then
46-
return token
43+
local function decode_json(data)
44+
local ok, decoded = pcall(function()
45+
return json.decode(data)
46+
end)
47+
if not ok then
48+
return nil, "Invalid JSON"
4749
end
48-
footer = basexx.to_url64(footer)
49-
local trailing = string.sub(token, #token - #footer + 1, #token)
50-
if trailing ~= footer then
51-
return nil, "Invalid message footer"
52-
end
53-
return string.sub(token, 1, #token - #footer - 1)
50+
return decoded
5451
end
5552

56-
local function aead_encrypt(key, payload, header, footer, nonce_key)
57-
if #nonce_key == 0 then
58-
nonce_key = luasodium.randombytes(luasodium.SYMMETRIC_NONCEBYTES)
59-
end
60-
local nonce = luasodium.generichash(payload, nonce_key, luasodium.SYMMETRIC_NONCEBYTES)
61-
local additional_data = v2.__pre_auth_encode(header, nonce, footer)
62-
local ciphertext, err = luasodium.aead_encrypt(payload, additional_data, nonce, key)
63-
if not ciphertext then
64-
return nil, err
65-
end
66-
local token = header .. basexx.to_url64(nonce .. ciphertext) ..
67-
(#footer > 0 and "." .. basexx.to_url64(footer) or "")
53+
-- API --
6854

69-
return token
70-
end
71-
72-
local function aead_decrypt(key, encrypted, header, footer)
73-
if header ~= string.sub(encrypted, 1, #header) then
74-
return nil, "Invalid message header"
75-
end
76-
local decoded = basexx.from_url64(string.sub(encrypted, #header + 1))
77-
local nonce = string.sub(decoded, 1, luasodium.SYMMETRIC_NONCEBYTES)
78-
local ciphertext = string.sub(decoded, luasodium.SYMMETRIC_NONCEBYTES + 1, #decoded)
79-
local additional_data = v2.__pre_auth_encode(header, nonce, footer)
80-
local decrypted, err = luasodium.aead_decrypt(ciphertext, additional_data, nonce, key)
81-
return decrypted, err
82-
end
83-
84-
function v2.get_symmetric_key_byte_length()
85-
return luasodium.SYMMETRIC_KEYBYTES
86-
end
55+
v2.SYMMETRIC_KEY_BYTES = paseto.SYMMETRIC_KEY_BYTES
56+
v2.ASYMMETRIC_PUBLIC_KEY_BYTES = paseto.ASYMMETRIC_PUBLIC_KEY_BYTES
57+
v2.ASYMMETRIC_SECRET_KEY_BYTES = paseto.ASYMMETRIC_SECRET_KEY_BYTES
8758

8859
function v2.generate_symmetric_key()
89-
return luasodium.randombytes(luasodium.SYMMETRIC_KEYBYTES)
60+
return paseto.generate_symmetric_key()
9061
end
9162

9263
function v2.generate_asymmetric_secret_key()
93-
return luasodium.sign_keypair()
64+
return paseto.generate_asymmetric_secret_key()
65+
end
66+
67+
function v2.extract_version_purpose(token)
68+
return paseto.extract_version_purpose(token)
9469
end
9570

96-
function v2.__encrypt(key, payload, footer, nonce)
97-
nonce = nonce or ""
98-
return aead_encrypt(key, payload, PROTOCOL_VERSION .. ".local.", footer, nonce)
71+
function v2.extract_footer_claims(token)
72+
local footer, err = paseto.extract_footer(token)
73+
if footer == nil then
74+
return nil, nil, err
75+
end
76+
if footer == "" then
77+
return {}, footer
78+
end
79+
local footer_claims
80+
footer_claims, err = decode_json(footer)
81+
if footer_claims == nil then
82+
return nil, nil, err
83+
end
84+
return footer_claims, footer
9985
end
10086

101-
function v2.encrypt(key, payload, footer)
102-
footer = footer or ""
103-
return v2.__encrypt(key, payload, footer)
87+
function v2.encrypt(key, payload_claims, footer_claims)
88+
local payload, footer
89+
payload = encode_json(payload_claims)
90+
if payload == nil or type(payload_claims) ~= "table" then
91+
return nil, "Invalid payload claims"
92+
end
93+
if footer_claims == nil then
94+
footer = ""
95+
else
96+
footer = encode_json(footer_claims)
97+
if footer == nil or type(footer_claims) ~= "table" then
98+
return nil, "Invalid footer claims"
99+
end
100+
end
101+
return paseto.encrypt(key, payload, footer)
104102
end
105103

106104
function v2.decrypt(key, token, footer)
107-
footer = footer or ""
108-
local encrypted_payload, err = v2.__validate_and_remove_footer(token, footer)
109-
if not encrypted_payload then
105+
local err, decrypted, payload_claims
106+
decrypted, err = paseto.decrypt(key, token, footer)
107+
if decrypted == nil then
110108
return nil, err
111109
end
112-
return aead_decrypt(key, encrypted_payload, PROTOCOL_VERSION .. ".local.", footer)
110+
payload_claims, err = decode_json(decrypted)
111+
if payload_claims == nil then
112+
return nil, err
113+
end
114+
return payload_claims
113115
end
114116

115-
function v2.sign(secret_key, message, footer)
116-
footer = footer or ""
117-
local header = PROTOCOL_VERSION .. ".public."
118-
local data = v2.__pre_auth_encode(header, message, footer)
119-
local signature = luasodium.sign_detached(data, secret_key)
120-
local token = header .. basexx.to_url64(message .. signature) ..
121-
(#footer > 0 and "." .. basexx.to_url64(footer) or "")
122-
return token
117+
function v2.sign(secret_key, payload_claims, footer_claims)
118+
local payload, footer
119+
payload = encode_json(payload_claims)
120+
if payload == nil or type(payload_claims) ~= "table" then
121+
return nil, "Invalid payload claims"
122+
end
123+
if footer_claims == nil then
124+
footer = ""
125+
else
126+
footer = encode_json(footer_claims)
127+
if footer == nil or type(footer_claims) ~= "table" then
128+
return nil, "Invalid footer claims"
129+
end
130+
end
131+
return paseto.sign(secret_key, payload, footer)
123132
end
124133

125134
function v2.verify(public_key, token, footer)
126-
footer = footer or ""
127-
local signed_payload, err = v2.__validate_and_remove_footer(token, footer)
128-
if not signed_payload then
135+
local err, verified_claims, payload_claims
136+
verified_claims, err = paseto.verify(public_key, token, footer)
137+
if verified_claims == nil then
129138
return nil, err
130139
end
131-
local header = PROTOCOL_VERSION .. ".public."
132-
if header ~= string.sub(signed_payload, 1, #header) then
133-
return nil, "Invalid message header"
134-
end
135-
local decoded = basexx.from_url64(string.sub(signed_payload, #header + 1))
136-
local message = string.sub(decoded, 1, #decoded - luasodium.SIGN_BYTES)
137-
local signature = string.sub(decoded, #decoded - luasodium.SIGN_BYTES + 1)
138-
local data = v2.__pre_auth_encode(header, message, footer)
139-
local verified
140-
verified, err = luasodium.sign_verify_detached(data, signature, public_key)
141-
if not verified then
140+
payload_claims, err = decode_json(verified_claims)
141+
if payload_claims == nil then
142142
return nil, err
143143
end
144-
return message
144+
return payload_claims
145145
end
146146

147147
return v2

0 commit comments

Comments
 (0)