Skip to content

Commit 20e69d4

Browse files
committed
Added token struct and switched to multibase
1 parent 3799bc2 commit 20e69d4

File tree

5 files changed

+62
-48
lines changed

5 files changed

+62
-48
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ Random::Secure.random_bytes(key)
3030
branca = Branca.new key
3131
3232
token = branca.encode("Hello world!", 123206400)
33-
payload, timestamp = branca.decode(token)
33+
p token # e.g. 870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z
3434
35-
p String.new(payload) == "Hello world!" # true
36-
p timestamp == 123206400 # true
35+
decoded = branca.decode(token)
36+
37+
p String.new(decoded.payload) == "Hello world!" # true
38+
p decoded.timestamp == 123206400 # true
3739
```
3840

3941
Make sure you use a secure encryption key generated by `Random::Secure.random_bytes`

shard.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: branca
2-
version: 0.2.1
2+
version: 0.3.0
33

44
authors:
55
- Johannes Rabausch <mail@jrabausch.de>
@@ -8,8 +8,8 @@ dependencies:
88
monocypher:
99
github: konovod/monocypher.cr
1010
version: ~> 4.0.1
11-
base62:
12-
github: Sija/base62.cr
11+
multibase:
12+
github: radbas/multibase.cr
1313

1414
development_dependencies:
1515
ameba:

spec/branca_spec.cr

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -59,51 +59,51 @@ describe Branca do
5959
describe "#decode" do
6060
it "decodes hello world with zero timestamp" do
6161
token = "870S4BYxgHw0KnP3W9fgVUHEhT5g86vJ17etaC5Kh5uIraWHCI1psNQGv298ZmjPwoYbjDQ9chy2z"
62-
payload, timestamp = branca.decode(token)
63-
payload.hexstring.should eq "48656c6c6f20776f726c6421"
64-
timestamp.should eq 0
62+
res = branca.decode(token)
63+
res.payload.hexstring.should eq "48656c6c6f20776f726c6421"
64+
res.timestamp.should eq 0
6565
end
6666
it "decodes hello world with max timestamp" do
6767
token = "89i7YCwu5tWAJNHUDdmIqhzOi5hVHOd4afjZcGMcVmM4enl4yeLiDyYv41eMkNmTX6IwYEFErCSqr"
68-
payload, timestamp = branca.decode(token)
69-
payload.hexstring.should eq "48656c6c6f20776f726c6421"
70-
timestamp.should eq 4294967295
68+
res = branca.decode(token)
69+
res.payload.hexstring.should eq "48656c6c6f20776f726c6421"
70+
res.timestamp.should eq 4294967295
7171
end
7272
it "decodes hello world with November 27 timestamp" do
7373
token = "875GH23U0Dr6nHFA63DhOyd9LkYudBkX8RsCTOMz5xoYAMw9sMd5QwcEqLDRnTDHPenOX7nP2trlT"
74-
payload, timestamp = branca.decode(token)
75-
payload.hexstring.should eq "48656c6c6f20776f726c6421"
76-
timestamp.should eq 123206400
74+
res = branca.decode(token)
75+
res.payload.hexstring.should eq "48656c6c6f20776f726c6421"
76+
res.timestamp.should eq 123206400
7777
end
7878
it "decodes eight null bytes with zero timestamp" do
7979
token = "1jIBheHbDdkCDFQmtgw4RUZeQoOJgGwTFJSpwOAk3XYpJJr52DEpILLmmwYl4tjdSbbNqcF1"
80-
payload, timestamp = branca.decode(token)
81-
payload.hexstring.should eq "0000000000000000"
82-
timestamp.should eq 0
80+
res = branca.decode(token)
81+
res.payload.hexstring.should eq "0000000000000000"
82+
res.timestamp.should eq 0
8383
end
8484
it "decodes eight null bytes with max timestamp" do
8585
token = "1jrx6DUu5q06oxykef2e2ZMyTcDRTQot9ZnwgifUtzAphGtjsxfbxXNhQyBEOGtpbkBgvIQx"
86-
payload, timestamp = branca.decode(token)
87-
payload.hexstring.should eq "0000000000000000"
88-
timestamp.should eq 4294967295
86+
res = branca.decode(token)
87+
res.payload.hexstring.should eq "0000000000000000"
88+
res.timestamp.should eq 4294967295
8989
end
9090
it "decodes eight null bytes with November 27th timestamp" do
9191
token = "1jJDJOEjuwVb9Csz1Ypw1KBWSkr0YDpeBeJN6NzJWx1VgPLmcBhu2SbkpQ9JjZ3nfUf7Aytp"
92-
payload, timestamp = branca.decode(token)
93-
payload.hexstring.should eq "0000000000000000"
94-
timestamp.should eq 123206400
92+
res = branca.decode(token)
93+
res.payload.hexstring.should eq "0000000000000000"
94+
res.timestamp.should eq 123206400
9595
end
9696
it "decodes empty payload" do
9797
token = "4sfD0vPFhIif8cy4nB3BQkHeJqkOkDvinI4zIhMjYX4YXZU5WIq9ycCVjGzB5"
98-
payload, timestamp = branca.decode(token)
99-
payload.hexstring.should eq ""
100-
timestamp.should eq 0
98+
res = branca.decode(token)
99+
res.payload.hexstring.should eq ""
100+
res.timestamp.should eq 0
101101
end
102102
it "decodes non-UTF8 payload" do
103103
token = "K9u6d0zjXp8RXNUGDyXAsB9AtPo60CD3xxQ2ulL8aQoTzXbvockRff0y1eXoHm"
104-
payload, timestamp = branca.decode(token)
105-
payload.hexstring.should eq "80"
106-
timestamp.should eq 123206400
104+
res = branca.decode(token)
105+
res.payload.hexstring.should eq "80"
106+
res.timestamp.should eq 123206400
107107
end
108108
it "throws with wrong version 0xBB" do
109109
token = "89mvl3RkwXjpEj5WMxK7GUDEHEeeeZtwjMIOogTthvr44qBfYtQSIZH5MHOTC0GzoutDIeoPVZk3w"

spec/spec_helper.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ require "../src/branca"
55
#
66
# This sets a user defined nonce for testing.
77
# Do not do this in production.
8-
struct Branca
8+
class Branca
99
@nonce = "beefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef".hexbytes
1010
end

src/branca.cr

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
require "monocypher"
2-
require "base62"
2+
require "multibase/base_62"
33

4-
struct Branca
5-
VERSION = UInt8.new 0xBA
4+
class Branca
5+
VERSION = 0xBA
66

77
alias BigEndian = IO::ByteFormat::BigEndian
8+
alias Base62 = Multibase::Base62
89
alias Nonce = Crypto::Nonce
910
alias Mac = Crypto::Header
1011

12+
struct Token
13+
property :payload, :timestamp
14+
15+
def initialize(
16+
@payload : Bytes = Bytes.empty,
17+
@timestamp : UInt32 = Time.utc.to_unix.to_u32
18+
)
19+
end
20+
end
21+
1122
class ExpiredTokenError < Exception
12-
getter :delta
23+
getter :token
1324

14-
def initialize(@delta : UInt32)
15-
super "token is expired by: #{@delta}s"
25+
def initialize(@token : Token)
26+
super "token is expired"
1627
end
1728
end
1829

@@ -29,7 +40,7 @@ struct Branca
2940
@key = StaticArray(UInt8, 32).new { |i| key[i] }
3041
end
3142

32-
# Creates a XChaCha20-Poly1305 AEAD encrypted Branca token.
43+
# Creates a XChaCha20-Poly1305 AEAD encrypted Branca Token.
3344
#
3445
# Returns a Base62 encoded String.
3546
def encode(payload : String | Bytes, timestamp : UInt32 = Time.utc.to_unix.to_u32) : String
@@ -40,7 +51,7 @@ struct Branca
4051

4152
# Use custom nonce if set (only for testing).
4253
nonce = @nonce || Nonce.new.to_slice
43-
header = Slice(UInt8).new(1, VERSION) + time + nonce
54+
header = Slice(UInt8).new(1, VERSION.to_u8) + time + nonce
4455

4556
ciphertext = Bytes.new(payload.size + Mac.size)
4657
LibMonocypher.aead_lock(
@@ -57,12 +68,15 @@ struct Branca
5768
Base62.encode(token)
5869
end
5970

60-
# Decodes a valid Branca token.
71+
def encode(token : Token) : String
72+
encode(token.payload, token.timestamp)
73+
end
74+
75+
# Decodes a Base62 encoded Branca Token.
6176
#
6277
# If *ttl* is greater than 0 and the token is expired, an `ExpiredTokenError` is raised.
63-
# Returns a {payload, timestamp} Tuple.
64-
def decode(token : String, ttl : UInt32 = 0) : {Bytes, UInt32}
65-
bytes = Base62.decode(token).to_s(16).hexbytes
78+
def decode(str : String, ttl : UInt32 = 0) : Token
79+
bytes = Base62.decode(str)
6680
header_size = 5 + Nonce.size
6781
raise "invalid token header size: got #{bytes.size}, expected #{header_size}" if bytes.size < header_size
6882

@@ -87,11 +101,9 @@ struct Branca
87101
raise "decryption error occurred: #{res}" unless res == 0
88102

89103
timestamp = BigEndian.decode(UInt32, header[1..4])
90-
if ttl > 0
91-
delta = Time.utc.to_unix - (timestamp + ttl)
92-
raise ExpiredTokenError.new delta.to_u32 if delta > 0
93-
end
104+
token = Token.new(payload, timestamp)
94105

95-
{payload, timestamp}
106+
raise ExpiredTokenError.new token if ttl > 0 && (timestamp + ttl) < Time.utc.to_unix
107+
token
96108
end
97109
end

0 commit comments

Comments
 (0)