-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8208dbe
Showing
11 changed files
with
517 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
root = true | ||
|
||
[*.cr] | ||
charset = utf-8 | ||
end_of_line = lf | ||
insert_final_newline = true | ||
indent_style = space | ||
indent_size = 2 | ||
trim_trailing_whitespace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
name: Build | ||
|
||
on: [push] | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
container: | ||
image: crystallang/crystal | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Install packages | ||
run: apt update && apt install -y wget | ||
- name: Patch shard.yml until https://github.com/didactic-drunk/zstd.cr/pull/2 is released | ||
run: "sed -i 's/version: ~> 1.1.0/branch: master/' shard.yml" | ||
- name: Install dependencies | ||
run: shards install | ||
- name: Run tests | ||
run: make ci |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/docs/ | ||
/lib/ | ||
/bin/ | ||
/.shards/ | ||
*.dwarf | ||
.DS_Store | ||
|
||
# Libraries don't need dependency lock | ||
# Dependencies will be locked in applications that use them | ||
/shard.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2020 [email protected] | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
.PHONY: docs | ||
|
||
ci: test | ||
|
||
test: lint | ||
@crystal spec | ||
|
||
lint: bin/ameba | ||
@bin/ameba | ||
|
||
docs: | ||
@crystal doc | ||
|
||
# Run this to initialize your development environment | ||
install: | ||
shards | ||
|
||
bin/ameba: | ||
@make install | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# Suzuri | ||
![Build](https://github.com/busyloop/suzuri/workflows/Build/badge.svg) [![GitHub](https://img.shields.io/github/license/busyloop/suzuri)](https://en.wikipedia.org/wiki/MIT_License) [![GitHub release](https://img.shields.io/github/release/busyloop/suzuri.svg)](https://github.com/busyloop/suzuri/releases) | ||
|
||
Suzuri is a secure and easy to use token format that employs | ||
[IETF XChaCha20-Poly1305 AEAD](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction) symmetric encryption to | ||
create authenticated, encrypted, tamperproof tokens. | ||
|
||
It compresses and encrypts an arbitrary sequence of bytes, | ||
then encodes the result to url-safe Base64. | ||
|
||
Suzuri tokens can be used as a secure alternative to JWT | ||
or for any type of general purpose message passing. | ||
|
||
|
||
## Installation | ||
|
||
1. Add the dependency to your `shard.yml`: | ||
|
||
```yaml | ||
dependencies: | ||
suzuri: | ||
github: busyloop/suzuri | ||
``` | ||
2. Run `shards install` | ||
|
||
## Documentation | ||
|
||
* [API Documentation](https://busyloop.github.io/suzuri/Suzuri.html) | ||
|
||
|
||
## Usage | ||
|
||
```crystal | ||
require "suzuri" | ||
TEST_KEY = "TheKeyLengthMustBeThirtyTwoBytes" | ||
## Encode | ||
token_str = Suzuri.encode("hello world", TEST_KEY) # => "(url-safe base64)" | ||
## Decode | ||
token = Suzuri.decode(token_str, TEST_KEY) # => Suzuri::Token | ||
token.to_s # => "hello world" | ||
token.timestamp # => 2020-01-01 01:23:45.0 UTC | ||
## Decode with a TTL constraint | ||
token_str = Suzuri.encode("hello world", TEST_KEY) # => "(url-safe base64)" | ||
sleep 5 | ||
Suzuri.decode(token_str, TEST_KEY, 2.seconds) # => Suzuri::Error::TokenExpired | ||
``` | ||
|
||
## Usage (with [JSON::Serializable](https://crystal-lang.org/api/0.34.0/JSON/Serializable.html)) | ||
|
||
```crystal | ||
require "suzuri/json_serializable" | ||
TEST_KEY = "TheKeyLengthMustBeThirtyTwoBytes" | ||
class Person | ||
include JSON::Serializable | ||
@[JSON::Field] | ||
property name : String | ||
def initialize(@name) | ||
end | ||
end | ||
bob = Person.new(name: "bob") | ||
token_str = bob.to_suzuri(TEST_KEY) | ||
bob2 = Person.from_suzuri(token_str, TEST_KEY) | ||
bob2.name # => "bob" | ||
``` | ||
|
||
|
||
## Compression | ||
|
||
By default Suzuri applies zstd compression before encryption when the | ||
payload is larger than 512 bytes. The compression threshold and level | ||
can be chosen at runtime. | ||
|
||
|
||
## Contributing | ||
|
||
1. Fork it (<https://github.com/busyloop/suzuri/fork>) | ||
2. Create your feature branch (`git checkout -b my-new-feature`) | ||
3. Commit your changes (`git commit -am 'Add some feature'`) | ||
4. Push to the branch (`git push origin my-new-feature`) | ||
5. Create a new Pull Request | ||
|
||
## Credits | ||
|
||
Suzuri is inspired by (but not compatible to) [Branca](https://github.com/tuupola/branca-spec/)-tokens. The underlying encryption is identical. | ||
Suzuri adds compression support and serializes to url-safe Base64 instead of Base62. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
name: suzuri | ||
version: 1.0.0 | ||
|
||
authors: | ||
- moe <[email protected]> | ||
|
||
description: | | ||
Authenticated and encrypted tokens | ||
crystal: 0.34.0 | ||
|
||
license: MIT | ||
|
||
dependencies: | ||
sodium: | ||
github: didactic-drunk/sodium.cr | ||
version: ~> 1.1.1 | ||
|
||
zstd: | ||
github: didactic-drunk/zstd.cr | ||
version: ~> 1.1.0 | ||
|
||
development_dependencies: | ||
timecop: | ||
github: crystal-community/timecop.cr | ||
|
||
ameba: | ||
github: crystal-ameba/ameba | ||
version: ~> 0.12.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
require "spec" | ||
require "json" | ||
require "timecop" | ||
require "../src/suzuri/json_serializable" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
require "./spec_helper" | ||
|
||
TEST_KEY = "TheKeyLengthMustBeThirtyTwoBytes" | ||
|
||
class JsonDemo | ||
include JSON::Serializable | ||
|
||
@[JSON::Field] | ||
property text : String | ||
|
||
@[JSON::Field] | ||
property float : Float64 | ||
|
||
@[JSON::Field] | ||
property time : Time | ||
|
||
def initialize(@text, @float, @time) | ||
end | ||
end | ||
|
||
describe Suzuri do | ||
it "decodes what it previously encoded" do | ||
token = Suzuri.encode("hello world", TEST_KEY) | ||
decoded = Suzuri.decode(token, TEST_KEY) | ||
String.new(decoded.payload).should eq "hello world" | ||
end | ||
|
||
it "compresses the payload above a size-threshold" do | ||
payload = "x" * 16384 | ||
|
||
token_nc = Suzuri.encode(payload, TEST_KEY, compress_threshold: UInt64::MAX) | ||
token_c = Suzuri.encode(payload, TEST_KEY, compress_threshold: 0) | ||
|
||
token_c.size.should be < token_nc.size | ||
|
||
# ensure both tokens decode | ||
Suzuri.decode(token_nc, TEST_KEY).to_s.should eq payload | ||
Suzuri.decode(token_c, TEST_KEY).to_s.should eq payload | ||
end | ||
|
||
it "raises on encode when key is not 32 bytes long" do | ||
expect_raises(ArgumentError, /key size mismatch/) do | ||
Suzuri.encode("hello world", "too short") | ||
end | ||
|
||
expect_raises(ArgumentError, /key size mismatch/) do | ||
Suzuri.encode("hello world", TEST_KEY + "too long") | ||
end | ||
end | ||
|
||
it "raises on decode when key is not 32 bytes long" do | ||
token = Suzuri.encode("hello world", TEST_KEY) | ||
expect_raises(ArgumentError, /key size mismatch/) do | ||
Suzuri.decode(token, "too short") | ||
end | ||
|
||
expect_raises(ArgumentError, /key size mismatch/) do | ||
Suzuri.decode(token, TEST_KEY + "too long") | ||
end | ||
end | ||
|
||
it "includes a timestamp with encoded tokens" do | ||
Timecop.freeze(Time.utc(1990,1,1)) do |frozen_time| | ||
token = Suzuri.encode("hello world", TEST_KEY) | ||
decoded = Suzuri.decode(token, TEST_KEY) | ||
decoded.timestamp.should eq frozen_time | ||
end | ||
end | ||
|
||
it "allows encoded timestamp to be overridden" do | ||
Timecop.freeze(Time.utc(1990,1,1)) do | ||
token = Suzuri.encode("hello world", TEST_KEY, Time.utc(2000,1,1)) | ||
decoded = Suzuri.decode(token, TEST_KEY) | ||
decoded.timestamp.should eq Time.utc(2000,1,1) | ||
end | ||
end | ||
|
||
it "raises on decode when decryption fails (e.g. wrong key)" do | ||
token = Suzuri.encode("hello world", TEST_KEY, Time.utc(2000,1,1)) | ||
expect_raises(Suzuri::Error::DecryptionFailed) do | ||
Suzuri.decode(token, "WrongSecretxxxxxxxxxxxxxxxxxxxxx") | ||
end | ||
end | ||
|
||
it "raises on decode when ttl is expired" do | ||
token = Suzuri.encode("hello world", TEST_KEY, Time.utc(2000,1,1)) | ||
expect_raises(Suzuri::Error::TokenExpired) do | ||
Suzuri.decode(token, TEST_KEY, 5.seconds) | ||
end | ||
end | ||
|
||
it "raises on decode when input is not base64" do | ||
not_a_token = "I'm not a token" | ||
expect_raises(Suzuri::Error::MalformedInput) do | ||
Suzuri.decode(not_a_token, TEST_KEY) | ||
end | ||
end | ||
|
||
it "raises on decode when base64 content isn't a suzuri token" do | ||
not_a_token = Base64.urlsafe_encode("I'm not a token") | ||
expect_raises(Suzuri::Error::MalformedInput) do | ||
Suzuri.decode(not_a_token, TEST_KEY) | ||
end | ||
end | ||
end | ||
|
||
describe JSON::Serializable do | ||
it "encodes/decodes JSON::Serializable objects via to_suzuri/from_suzuri" do | ||
demo = JsonDemo.new(text: "hello world", float: 0.42, time: Time.utc(1,1,1)) | ||
|
||
token = demo.to_suzuri(TEST_KEY) | ||
|
||
decoded = JsonDemo.from_suzuri(token, TEST_KEY) | ||
decoded.text.should eq demo.text | ||
decoded.float.should eq demo.float | ||
decoded.time.should eq demo.time | ||
end | ||
|
||
it "decodes JSON::Serializable objects with timestamp via from_suzuri_with_timestamp" do | ||
demo = JsonDemo.new(text: "hello world", float: 0.42, time: Time.utc(1,1,1)) | ||
|
||
token = demo.to_suzuri(TEST_KEY) | ||
|
||
decoded, timestamp = JsonDemo.from_suzuri_with_timestamp(token, TEST_KEY) | ||
decoded.text.should eq demo.text | ||
decoded.float.should eq demo.float | ||
decoded.time.should eq demo.time | ||
timestamp.should be_a Time | ||
end | ||
end |
Oops, something went wrong.