├── .travis.yml ├── go.mod ├── .gitignore ├── go.sum ├── LICENSE ├── README.md ├── branca.go └── branca_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13" 5 | - "1.14" 6 | - tip 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hako/branca 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/eknkc/basex v1.0.0 7 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 8 | ) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/eknkc/basex v1.0.0 h1:R2zGRGJAcqEES03GqHU9leUF5n4Pg6ahazPbSTQWCWc= 2 | github.com/eknkc/basex v1.0.0/go.mod h1:k/F/exNEHFdbs3ZHuasoP2E7zeWwZblG84Y7Z59vQRo= 3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 4 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk= 5 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 7 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 8 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 9 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 Wesley Hill 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # branca 2 | 3 | [![Build Status](https://travis-ci.org/hako/branca.svg?branch=master)](https://travis-ci.org/hako/branca) [![Go Report Card](https://goreportcard.com/badge/github.com/hako/branca)](https://goreportcard.com/report/github.com/hako/branca) 4 | [![GoDoc](https://godoc.org/github.com/hako/branca?status.svg)](https://godoc.org/github.com/hako/branca) 5 | 6 | branca is a secure alternative to JWT, This implementation is written in pure Go (no cgo dependencies) and implements the [branca token specification](https://github.com/tuupola/branca-spec). 7 | 8 | # Requirements 9 | 10 | Go 1.13+ 11 | 12 | # Install 13 | 14 | ``` 15 | go get -u github.com/hako/branca 16 | ``` 17 | 18 | # Example 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "github.com/hako/branca" 26 | ) 27 | 28 | func main() { 29 | b := branca.NewBranca("supersecretkeyyoushouldnotcommit") // This key must be exactly 32 bytes long. 30 | 31 | // Encode String to Branca Token. 32 | token, err := b.EncodeToString("Hello world!") 33 | if err != nil { 34 | fmt.Println(err) 35 | } 36 | 37 | //b.SetTTL(3600) // Uncomment this to set an expiration (or ttl) of the token (in seconds). 38 | //token = "87y8daMzSkn7PA7JsvrTT0JUq1OhCjw9K8w2eyY99DKru9FrVKMfeXWW8yB42C7u0I6jNhOdL5ZqL" // This token will be not allowed if a ttl is set. 39 | 40 | // Decode Branca Token. 41 | message, err := b.DecodeToString(token) 42 | if err != nil { 43 | fmt.Println(err) // token is expired. 44 | return 45 | } 46 | fmt.Println(token) // 87y8da.... 47 | fmt.Println(message) // Hello world! 48 | } 49 | ``` 50 | 51 | # Todo 52 | 53 | Here are a few things that need to be done: 54 | 55 | - [x] Remove cgo dependencies. 56 | - [x] Move to a pure XChaCha20 algorithm in Go. 57 | - [x] Add more tests than just acceptance tests. 58 | - [x] Increase test coverage. 59 | - [ ] Additional Methods. (Encode, Decode []byte) 60 | - [ ] Performance benchmarks. 61 | - [ ] More comments, examples and documentation. 62 | 63 | # Contributing 64 | 65 | Contributions are welcome! Fork this repo and add your changes and submit a PR. 66 | 67 | If you would like to fix a bug, add a feature or provide feedback you can do so in the issues section. 68 | 69 | You can run tests by runnning `go test`. Running `go test; go vet; golint` is recommended. 70 | 71 | # License 72 | 73 | MIT 74 | -------------------------------------------------------------------------------- /branca.go: -------------------------------------------------------------------------------- 1 | // Package branca implements the branca token specification. 2 | package branca 3 | 4 | import ( 5 | "bytes" 6 | "crypto/rand" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | "time" 12 | 13 | "github.com/eknkc/basex" 14 | "golang.org/x/crypto/chacha20poly1305" 15 | ) 16 | 17 | const ( 18 | version byte = 0xBA // Branca magic byte 19 | base62 string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 20 | ) 21 | 22 | var ( 23 | // ErrInvalidToken indicates an invalid token. 24 | ErrInvalidToken = errors.New("invalid base62 token") 25 | // ErrInvalidTokenVersion indicates an invalid token version. 26 | ErrInvalidTokenVersion = errors.New("invalid token version") 27 | // ErrBadKeyLength indicates a bad key length. 28 | ErrBadKeyLength = errors.New("bad key length") 29 | ) 30 | 31 | // ErrExpiredToken indicates an expired token. 32 | type ErrExpiredToken struct { 33 | // Time is the token expiration time. 34 | Time time.Time 35 | } 36 | 37 | func (e *ErrExpiredToken) Error() string { 38 | delta := time.Unix(time.Now().Unix(), 0).Sub(time.Unix(e.Time.Unix(), 0)) 39 | return fmt.Sprintf("token is expired by %v", delta) 40 | } 41 | 42 | // Branca holds a key of exactly 32 bytes. The nonce and timestamp are used for acceptance tests. 43 | type Branca struct { 44 | Key string 45 | nonce string 46 | ttl uint32 47 | timestamp uint32 48 | } 49 | 50 | // SetTTL sets a Time To Live on the token for valid tokens. 51 | func (b *Branca) SetTTL(ttl uint32) { 52 | b.ttl = ttl 53 | } 54 | 55 | // setTimeStamp sets a timestamp for testing. 56 | func (b *Branca) setTimeStamp(timestamp uint32) { 57 | b.timestamp = timestamp 58 | } 59 | 60 | // setNonce sets a nonce for testing. 61 | func (b *Branca) setNonce(nonce string) { 62 | b.nonce = nonce 63 | } 64 | 65 | // NewBranca creates a *Branca struct. 66 | func NewBranca(key string) (b *Branca) { 67 | return &Branca{ 68 | Key: key, 69 | } 70 | } 71 | 72 | // EncodeToString encodes the data matching the format: 73 | // Version (byte) || Timestamp ([4]byte) || Nonce ([24]byte) || Ciphertext ([]byte) || Tag ([16]byte) 74 | func (b *Branca) EncodeToString(data string) (string, error) { 75 | var timestamp uint32 76 | var nonce []byte 77 | if b.timestamp == 0 { 78 | b.timestamp = uint32(time.Now().Unix()) 79 | } 80 | timestamp = b.timestamp 81 | 82 | if len(b.nonce) == 0 { 83 | nonce = make([]byte, 24) 84 | if _, err := rand.Read(nonce); err != nil { 85 | return "", err 86 | } 87 | } else { 88 | noncebytes, err := hex.DecodeString(b.nonce) 89 | if err != nil { 90 | return "", ErrInvalidToken 91 | } 92 | nonce = noncebytes 93 | } 94 | 95 | key := bytes.NewBufferString(b.Key).Bytes() 96 | payload := bytes.NewBufferString(data).Bytes() 97 | 98 | timeBuffer := make([]byte, 4) 99 | binary.BigEndian.PutUint32(timeBuffer, timestamp) 100 | header := append(timeBuffer, nonce...) 101 | header = append([]byte{version}, header...) 102 | 103 | xchacha, err := chacha20poly1305.NewX(key) 104 | if err != nil { 105 | return "", ErrBadKeyLength 106 | } 107 | 108 | ciphertext := xchacha.Seal(nil, nonce, payload, header) 109 | 110 | token := append(header, ciphertext...) 111 | base62, err := basex.NewEncoding(base62) 112 | if err != nil { 113 | return "", err 114 | } 115 | return base62.Encode(token), nil 116 | } 117 | 118 | // DecodeToString decodes the data. 119 | func (b *Branca) DecodeToString(data string) (string, error) { 120 | if len(data) < 62 { 121 | return "", fmt.Errorf("%w: length is less than 62", ErrInvalidToken) 122 | } 123 | base62, err := basex.NewEncoding(base62) 124 | if err != nil { 125 | return "", fmt.Errorf("%v", err) 126 | } 127 | token, err := base62.Decode(data) 128 | if err != nil { 129 | return "", ErrInvalidToken 130 | } 131 | header := token[:29] 132 | ciphertext := token[29:] 133 | tokenversion := header[0] 134 | timestamp := binary.BigEndian.Uint32(header[1:5]) 135 | nonce := header[5:] 136 | 137 | if tokenversion != version { 138 | return "", fmt.Errorf("%w: got %#X but expected %#X", ErrInvalidTokenVersion, tokenversion, version) 139 | } 140 | 141 | key := bytes.NewBufferString(b.Key).Bytes() 142 | 143 | xchacha, err := chacha20poly1305.NewX(key) 144 | if err != nil { 145 | return "", ErrBadKeyLength 146 | } 147 | payload, err := xchacha.Open(nil, nonce, ciphertext, header) 148 | if err != nil { 149 | return "", err 150 | } 151 | 152 | if b.ttl != 0 { 153 | future := int64(timestamp + b.ttl) 154 | now := time.Now().Unix() 155 | if future < now { 156 | return "", &ErrExpiredToken{Time: time.Unix(future, 0)} 157 | } 158 | } 159 | 160 | payloadString := bytes.NewBuffer(payload).String() 161 | return payloadString, nil 162 | } 163 | -------------------------------------------------------------------------------- /branca_test.go: -------------------------------------------------------------------------------- 1 | package branca 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var ( 10 | testVectors []struct { 11 | key string 12 | nonce string 13 | timestamp uint32 14 | payload string 15 | expected string 16 | } 17 | ) 18 | 19 | // TestVector1 for testing encoding data to a valid branca token. 20 | func TestVector1(t *testing.T) { 21 | testVectors = []struct { 22 | key string 23 | nonce string 24 | timestamp uint32 25 | payload string 26 | expected string 27 | }{ 28 | {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, 29 | } 30 | 31 | for _, table := range testVectors { 32 | b := NewBranca(table.key) 33 | b.setNonce(table.nonce) 34 | b.setTimeStamp(table.timestamp) 35 | 36 | // Encode string. 37 | encoded, err := b.EncodeToString(table.payload) 38 | if err != nil { 39 | t.Errorf("%q", err) 40 | } 41 | if encoded != table.expected { 42 | t.Errorf("EncodeToString(\"%s\") = %s. got %s, expected %q", table.payload, encoded, encoded, table.expected) 43 | } 44 | 45 | // Decode string. 46 | decoded, err := b.DecodeToString(encoded) 47 | if err != nil { 48 | t.Errorf("%q", err) 49 | } 50 | if decoded != table.payload { 51 | t.Errorf("DecodeToString(\"%s\") = %s. got %s, expected %q", table.expected, decoded, decoded, table.expected) 52 | } 53 | } 54 | } 55 | 56 | // TestVector2 for testing encoding data to a valid branca token with a TTL. 57 | func TestVector2(t *testing.T) { 58 | testVectors = []struct { 59 | key string 60 | nonce string 61 | timestamp uint32 62 | payload string 63 | expected string 64 | }{ 65 | {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, 66 | } 67 | 68 | for _, table := range testVectors { 69 | b := NewBranca(table.key) 70 | b.setNonce(table.nonce) 71 | b.setTimeStamp(table.timestamp) 72 | 73 | // Encode string. 74 | encoded, err := b.EncodeToString(table.payload) 75 | if err != nil { 76 | t.Errorf("%q", err) 77 | } 78 | if encoded != table.expected { 79 | t.Errorf("EncodeToString(\"%s\") = %s. got %s, expected %q", table.payload, encoded, encoded, table.expected) 80 | } 81 | 82 | // Decode string with TTL. Should throw an error with no token encoded because it has expired. 83 | b.SetTTL(3600) 84 | decoded, derr := b.DecodeToString(encoded) 85 | if derr == nil { 86 | t.Errorf("%q", derr) 87 | } 88 | if decoded != "" { 89 | t.Errorf("DecodeToString(\"%s\") = %s. got %s, expected %q", table.expected, decoded, decoded, table.expected) 90 | } 91 | } 92 | } 93 | 94 | // TestGenerateToken for testing issuing branca tokens. 95 | func TestGenerateToken(t *testing.T) { 96 | testVectors = []struct { 97 | key string 98 | nonce string 99 | timestamp uint32 100 | payload string 101 | expected string 102 | }{ 103 | {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, 104 | } 105 | 106 | for _, table := range testVectors { 107 | // Not generated with set timestamp. 108 | b := NewBranca(table.key) 109 | 110 | // Encode string. 111 | encoded, err := b.EncodeToString(table.payload) 112 | if err != nil { 113 | t.Errorf("%q", err) 114 | } 115 | if encoded == table.expected { 116 | t.Errorf("EncodeToString(\"%s\") = %s. got %s, expected %q", table.payload, encoded, encoded, table.expected) 117 | } 118 | } 119 | } 120 | 121 | // TestInvalidEncodeString for testing errors when generating branca tokens. 122 | func TestInvalidEncodeString(t *testing.T) { 123 | testVectors = []struct { 124 | key string 125 | nonce string 126 | timestamp uint32 127 | payload string 128 | expected string 129 | }{ 130 | {"supersecretkeyyoushouldnotcommi", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, // Invalid key 131 | 132 | {"supersecretkeyyoushouldnotcommi", "", 123206400, "Hello world!", 133 | "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, // Invalid key + no nonce 134 | 135 | } 136 | 137 | for _, table := range testVectors { 138 | b := NewBranca(table.key) 139 | 140 | _, err := b.EncodeToString(table.payload) 141 | if err == nil { 142 | t.Errorf("%q", err) 143 | } 144 | } 145 | } 146 | 147 | // TestInvalidDecodeString for testing errors when decoding branca tokens. 148 | func TestInvalidDecodeString(t *testing.T) { 149 | testVectors = []struct { 150 | key string 151 | nonce string 152 | timestamp uint32 153 | payload string 154 | expected string 155 | }{ 156 | {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0"}, // Invalid base62 157 | 158 | {"supersecretkeyyoushouldnotcommi", "", 123206400, "Hello world!", 159 | "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsA"}, // Invalid key + Invalid base62. 160 | 161 | {"supersecretkeyyoushouldnotcommi", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, // Invalid key 162 | 163 | {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLOZtQ0ekPHt8kJHQp0a"}, // Invalid malformed base62 164 | } 165 | 166 | for _, table := range testVectors { 167 | b := NewBranca(table.key) 168 | 169 | _, err := b.DecodeToString(table.expected) 170 | if err == nil { 171 | t.Errorf("%q", err) 172 | } 173 | } 174 | } 175 | 176 | // TestExpiredTokenError tests if decoding an expired tokens returns the corresponding error type. 177 | func TestExpiredTokenError(t *testing.T) { 178 | b := NewBranca("supersecretkeyyoushouldnotcommit") 179 | 180 | ttl := time.Second * 1 181 | b.SetTTL(uint32(ttl.Seconds())) 182 | token, encErr := b.EncodeToString("Hello World!") 183 | if encErr != nil { 184 | t.Errorf("%q", encErr) 185 | } 186 | 187 | // Wait (with enough additional waiting time) until the token is expired... 188 | time.Sleep(ttl * 3) 189 | // ...and decode the token again that is expired by now. 190 | _, decErr := b.DecodeToString(token) 191 | var errExpiredToken *ErrExpiredToken 192 | if !errors.As(decErr, &errExpiredToken) { 193 | t.Errorf("%v", decErr) 194 | } 195 | } 196 | 197 | // TestInvalidTokenError tests if decoding an invalid token returns the corresponding error type. 198 | func TestInvalidTokenError(t *testing.T) { 199 | b := NewBranca("supersecretkeyyoushouldnotcommit") 200 | 201 | _, err := b.DecodeToString("$") 202 | if !errors.Is(err, ErrInvalidToken) { 203 | t.Errorf("%v", err) 204 | } 205 | } 206 | 207 | // TestInvalidTokenVersionError tests if decoding an invalid token returns the corresponding error type. 208 | func TestInvalidTokenVersionError(t *testing.T) { 209 | // A token with an invalid version where the HEX value 0XBA has been replaced with 0xFF. 210 | // The original token is "1WgRcDTWm6MyptVOMG9TeEPVcYW01K6hW5SzLrzCkLlrOOovO5TmpDxQql12N2n0jELx". 211 | tokenWithInvalidVersion := "25jsrzc9Q6kmzrnCYWf5Z7LCOG2C7Uiu3NbTP0B9ppLDrxZkhLGOuFVB6FqrWp0ypJTF" 212 | 213 | b := NewBranca("supersecretkeyyoushouldnotcommit") 214 | _, err := b.DecodeToString(tokenWithInvalidVersion) 215 | if !errors.Is(err, ErrInvalidTokenVersion) { 216 | t.Errorf("%v", err) 217 | } 218 | } 219 | 220 | // TestBadKeyLengthError tests if (en/de)coding a token with an invalid key returns the corresponding error type. 221 | func TestBadKeyLengthError(t *testing.T) { 222 | validToken := "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a" 223 | testKeys := []string{ 224 | "", 225 | "thiskeyistooshort", 226 | "thiskeyislongerthantheexpected32bytes", 227 | } 228 | 229 | for _, key := range testKeys { 230 | b := NewBranca(key) 231 | 232 | _, err := b.DecodeToString(validToken) 233 | if !errors.Is(err, ErrBadKeyLength) { 234 | t.Errorf("%v", err) 235 | } 236 | } 237 | } 238 | --------------------------------------------------------------------------------