Repository: hako/branca Branch: master Commit: 6052ac720505 Files: 8 Total size: 15.6 KB Directory structure: gitextract_coouietz/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── branca.go ├── branca_test.go ├── go.mod └── go.sum ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof ================================================ FILE: .travis.yml ================================================ language: go go: - "1.13" - "1.14" - tip ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Wesley Hill 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. ================================================ FILE: README.md ================================================ # branca [![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) [![GoDoc](https://godoc.org/github.com/hako/branca?status.svg)](https://godoc.org/github.com/hako/branca) 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). # Requirements Go 1.13+ # Install ``` go get -u github.com/hako/branca ``` # Example ```go package main import ( "fmt" "github.com/hako/branca" ) func main() { b := branca.NewBranca("supersecretkeyyoushouldnotcommit") // This key must be exactly 32 bytes long. // Encode String to Branca Token. token, err := b.EncodeToString("Hello world!") if err != nil { fmt.Println(err) } //b.SetTTL(3600) // Uncomment this to set an expiration (or ttl) of the token (in seconds). //token = "87y8daMzSkn7PA7JsvrTT0JUq1OhCjw9K8w2eyY99DKru9FrVKMfeXWW8yB42C7u0I6jNhOdL5ZqL" // This token will be not allowed if a ttl is set. // Decode Branca Token. message, err := b.DecodeToString(token) if err != nil { fmt.Println(err) // token is expired. return } fmt.Println(token) // 87y8da.... fmt.Println(message) // Hello world! } ``` # Todo Here are a few things that need to be done: - [x] Remove cgo dependencies. - [x] Move to a pure XChaCha20 algorithm in Go. - [x] Add more tests than just acceptance tests. - [x] Increase test coverage. - [ ] Additional Methods. (Encode, Decode []byte) - [ ] Performance benchmarks. - [ ] More comments, examples and documentation. # Contributing Contributions are welcome! Fork this repo and add your changes and submit a PR. If you would like to fix a bug, add a feature or provide feedback you can do so in the issues section. You can run tests by runnning `go test`. Running `go test; go vet; golint` is recommended. # License MIT ================================================ FILE: branca.go ================================================ // Package branca implements the branca token specification. package branca import ( "bytes" "crypto/rand" "encoding/binary" "encoding/hex" "errors" "fmt" "time" "github.com/eknkc/basex" "golang.org/x/crypto/chacha20poly1305" ) const ( version byte = 0xBA // Branca magic byte base62 string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ) var ( // ErrInvalidToken indicates an invalid token. ErrInvalidToken = errors.New("invalid base62 token") // ErrInvalidTokenVersion indicates an invalid token version. ErrInvalidTokenVersion = errors.New("invalid token version") // ErrBadKeyLength indicates a bad key length. ErrBadKeyLength = errors.New("bad key length") ) // ErrExpiredToken indicates an expired token. type ErrExpiredToken struct { // Time is the token expiration time. Time time.Time } func (e *ErrExpiredToken) Error() string { delta := time.Unix(time.Now().Unix(), 0).Sub(time.Unix(e.Time.Unix(), 0)) return fmt.Sprintf("token is expired by %v", delta) } // Branca holds a key of exactly 32 bytes. The nonce and timestamp are used for acceptance tests. type Branca struct { Key string nonce string ttl uint32 timestamp uint32 } // SetTTL sets a Time To Live on the token for valid tokens. func (b *Branca) SetTTL(ttl uint32) { b.ttl = ttl } // setTimeStamp sets a timestamp for testing. func (b *Branca) setTimeStamp(timestamp uint32) { b.timestamp = timestamp } // setNonce sets a nonce for testing. func (b *Branca) setNonce(nonce string) { b.nonce = nonce } // NewBranca creates a *Branca struct. func NewBranca(key string) (b *Branca) { return &Branca{ Key: key, } } // EncodeToString encodes the data matching the format: // Version (byte) || Timestamp ([4]byte) || Nonce ([24]byte) || Ciphertext ([]byte) || Tag ([16]byte) func (b *Branca) EncodeToString(data string) (string, error) { var timestamp uint32 var nonce []byte if b.timestamp == 0 { b.timestamp = uint32(time.Now().Unix()) } timestamp = b.timestamp if len(b.nonce) == 0 { nonce = make([]byte, 24) if _, err := rand.Read(nonce); err != nil { return "", err } } else { noncebytes, err := hex.DecodeString(b.nonce) if err != nil { return "", ErrInvalidToken } nonce = noncebytes } key := bytes.NewBufferString(b.Key).Bytes() payload := bytes.NewBufferString(data).Bytes() timeBuffer := make([]byte, 4) binary.BigEndian.PutUint32(timeBuffer, timestamp) header := append(timeBuffer, nonce...) header = append([]byte{version}, header...) xchacha, err := chacha20poly1305.NewX(key) if err != nil { return "", ErrBadKeyLength } ciphertext := xchacha.Seal(nil, nonce, payload, header) token := append(header, ciphertext...) base62, err := basex.NewEncoding(base62) if err != nil { return "", err } return base62.Encode(token), nil } // DecodeToString decodes the data. func (b *Branca) DecodeToString(data string) (string, error) { if len(data) < 62 { return "", fmt.Errorf("%w: length is less than 62", ErrInvalidToken) } base62, err := basex.NewEncoding(base62) if err != nil { return "", fmt.Errorf("%v", err) } token, err := base62.Decode(data) if err != nil { return "", ErrInvalidToken } header := token[:29] ciphertext := token[29:] tokenversion := header[0] timestamp := binary.BigEndian.Uint32(header[1:5]) nonce := header[5:] if tokenversion != version { return "", fmt.Errorf("%w: got %#X but expected %#X", ErrInvalidTokenVersion, tokenversion, version) } key := bytes.NewBufferString(b.Key).Bytes() xchacha, err := chacha20poly1305.NewX(key) if err != nil { return "", ErrBadKeyLength } payload, err := xchacha.Open(nil, nonce, ciphertext, header) if err != nil { return "", err } if b.ttl != 0 { future := int64(timestamp + b.ttl) now := time.Now().Unix() if future < now { return "", &ErrExpiredToken{Time: time.Unix(future, 0)} } } payloadString := bytes.NewBuffer(payload).String() return payloadString, nil } ================================================ FILE: branca_test.go ================================================ package branca import ( "errors" "testing" "time" ) var ( testVectors []struct { key string nonce string timestamp uint32 payload string expected string } ) // TestVector1 for testing encoding data to a valid branca token. func TestVector1(t *testing.T) { testVectors = []struct { key string nonce string timestamp uint32 payload string expected string }{ {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, } for _, table := range testVectors { b := NewBranca(table.key) b.setNonce(table.nonce) b.setTimeStamp(table.timestamp) // Encode string. encoded, err := b.EncodeToString(table.payload) if err != nil { t.Errorf("%q", err) } if encoded != table.expected { t.Errorf("EncodeToString(\"%s\") = %s. got %s, expected %q", table.payload, encoded, encoded, table.expected) } // Decode string. decoded, err := b.DecodeToString(encoded) if err != nil { t.Errorf("%q", err) } if decoded != table.payload { t.Errorf("DecodeToString(\"%s\") = %s. got %s, expected %q", table.expected, decoded, decoded, table.expected) } } } // TestVector2 for testing encoding data to a valid branca token with a TTL. func TestVector2(t *testing.T) { testVectors = []struct { key string nonce string timestamp uint32 payload string expected string }{ {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, } for _, table := range testVectors { b := NewBranca(table.key) b.setNonce(table.nonce) b.setTimeStamp(table.timestamp) // Encode string. encoded, err := b.EncodeToString(table.payload) if err != nil { t.Errorf("%q", err) } if encoded != table.expected { t.Errorf("EncodeToString(\"%s\") = %s. got %s, expected %q", table.payload, encoded, encoded, table.expected) } // Decode string with TTL. Should throw an error with no token encoded because it has expired. b.SetTTL(3600) decoded, derr := b.DecodeToString(encoded) if derr == nil { t.Errorf("%q", derr) } if decoded != "" { t.Errorf("DecodeToString(\"%s\") = %s. got %s, expected %q", table.expected, decoded, decoded, table.expected) } } } // TestGenerateToken for testing issuing branca tokens. func TestGenerateToken(t *testing.T) { testVectors = []struct { key string nonce string timestamp uint32 payload string expected string }{ {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, } for _, table := range testVectors { // Not generated with set timestamp. b := NewBranca(table.key) // Encode string. encoded, err := b.EncodeToString(table.payload) if err != nil { t.Errorf("%q", err) } if encoded == table.expected { t.Errorf("EncodeToString(\"%s\") = %s. got %s, expected %q", table.payload, encoded, encoded, table.expected) } } } // TestInvalidEncodeString for testing errors when generating branca tokens. func TestInvalidEncodeString(t *testing.T) { testVectors = []struct { key string nonce string timestamp uint32 payload string expected string }{ {"supersecretkeyyoushouldnotcommi", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, // Invalid key {"supersecretkeyyoushouldnotcommi", "", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, // Invalid key + no nonce } for _, table := range testVectors { b := NewBranca(table.key) _, err := b.EncodeToString(table.payload) if err == nil { t.Errorf("%q", err) } } } // TestInvalidDecodeString for testing errors when decoding branca tokens. func TestInvalidDecodeString(t *testing.T) { testVectors = []struct { key string nonce string timestamp uint32 payload string expected string }{ {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0"}, // Invalid base62 {"supersecretkeyyoushouldnotcommi", "", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsA"}, // Invalid key + Invalid base62. {"supersecretkeyyoushouldnotcommi", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a"}, // Invalid key {"supersecretkeyyoushouldnotcommit", "0102030405060708090a0b0c0102030405060708090a0b0c", 123206400, "Hello world!", "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLOZtQ0ekPHt8kJHQp0a"}, // Invalid malformed base62 } for _, table := range testVectors { b := NewBranca(table.key) _, err := b.DecodeToString(table.expected) if err == nil { t.Errorf("%q", err) } } } // TestExpiredTokenError tests if decoding an expired tokens returns the corresponding error type. func TestExpiredTokenError(t *testing.T) { b := NewBranca("supersecretkeyyoushouldnotcommit") ttl := time.Second * 1 b.SetTTL(uint32(ttl.Seconds())) token, encErr := b.EncodeToString("Hello World!") if encErr != nil { t.Errorf("%q", encErr) } // Wait (with enough additional waiting time) until the token is expired... time.Sleep(ttl * 3) // ...and decode the token again that is expired by now. _, decErr := b.DecodeToString(token) var errExpiredToken *ErrExpiredToken if !errors.As(decErr, &errExpiredToken) { t.Errorf("%v", decErr) } } // TestInvalidTokenError tests if decoding an invalid token returns the corresponding error type. func TestInvalidTokenError(t *testing.T) { b := NewBranca("supersecretkeyyoushouldnotcommit") _, err := b.DecodeToString("$") if !errors.Is(err, ErrInvalidToken) { t.Errorf("%v", err) } } // TestInvalidTokenVersionError tests if decoding an invalid token returns the corresponding error type. func TestInvalidTokenVersionError(t *testing.T) { // A token with an invalid version where the HEX value 0XBA has been replaced with 0xFF. // The original token is "1WgRcDTWm6MyptVOMG9TeEPVcYW01K6hW5SzLrzCkLlrOOovO5TmpDxQql12N2n0jELx". tokenWithInvalidVersion := "25jsrzc9Q6kmzrnCYWf5Z7LCOG2C7Uiu3NbTP0B9ppLDrxZkhLGOuFVB6FqrWp0ypJTF" b := NewBranca("supersecretkeyyoushouldnotcommit") _, err := b.DecodeToString(tokenWithInvalidVersion) if !errors.Is(err, ErrInvalidTokenVersion) { t.Errorf("%v", err) } } // TestBadKeyLengthError tests if (en/de)coding a token with an invalid key returns the corresponding error type. func TestBadKeyLengthError(t *testing.T) { validToken := "875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a" testKeys := []string{ "", "thiskeyistooshort", "thiskeyislongerthantheexpected32bytes", } for _, key := range testKeys { b := NewBranca(key) _, err := b.DecodeToString(validToken) if !errors.Is(err, ErrBadKeyLength) { t.Errorf("%v", err) } } } ================================================ FILE: go.mod ================================================ module github.com/hako/branca go 1.13 require ( github.com/eknkc/basex v1.0.0 golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 ) ================================================ FILE: go.sum ================================================ github.com/eknkc/basex v1.0.0 h1:R2zGRGJAcqEES03GqHU9leUF5n4Pg6ahazPbSTQWCWc= github.com/eknkc/basex v1.0.0/go.mod h1:k/F/exNEHFdbs3ZHuasoP2E7zeWwZblG84Y7Z59vQRo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk= golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=