[
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: go\n\ngo:\n  - \"1.13\"\n  - \"1.14\"\n  - tip\n"
  },
  {
    "path": "LICENSE",
    "content": "\nThe MIT License (MIT)\n\nCopyright (c) 2018 Wesley Hill\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# branca\n\n[![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)\n[![GoDoc](https://godoc.org/github.com/hako/branca?status.svg)](https://godoc.org/github.com/hako/branca) \n\nbranca 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).\n\n# Requirements\n\nGo 1.13+\n\n# Install\n\n```\ngo get -u github.com/hako/branca\n```\n\n# Example\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"github.com/hako/branca\"\n)\n\nfunc main() {\n\tb := branca.NewBranca(\"supersecretkeyyoushouldnotcommit\") // This key must be exactly 32 bytes long.\n\t\n\t// Encode String to Branca Token.\n\ttoken, err := b.EncodeToString(\"Hello world!\")\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\t\t\t\t\n    //b.SetTTL(3600) // Uncomment this to set an expiration (or ttl) of the token (in seconds).\n    //token = \"87y8daMzSkn7PA7JsvrTT0JUq1OhCjw9K8w2eyY99DKru9FrVKMfeXWW8yB42C7u0I6jNhOdL5ZqL\" // This token will be not allowed if a ttl is set.\n\t\n\t// Decode Branca Token.\n\tmessage, err := b.DecodeToString(token)\n\tif err != nil {\n\t\tfmt.Println(err) // token is expired.\n\t\treturn\n\t}\n\tfmt.Println(token) // 87y8da....\n\tfmt.Println(message) // Hello world!\n}\n```\n\n# Todo\n\nHere are a few things that need to be done:\n\n- [x] Remove cgo dependencies.\n- [x] Move to a pure XChaCha20 algorithm in Go.\n- [x] Add more tests than just acceptance tests.\n- [x] Increase test coverage.\n- [ ] Additional Methods. (Encode, Decode []byte)\n- [ ] Performance benchmarks.\n- [ ] More comments, examples and documentation.\n\n# Contributing\n\nContributions are welcome! Fork this repo and add your changes and submit a PR.\n\nIf you would like to fix a bug, add a feature or provide feedback you can do so in the issues section.\n\nYou can run tests by runnning `go test`. Running `go test; go vet; golint` is recommended.\n\n# License\n\nMIT\n"
  },
  {
    "path": "branca.go",
    "content": "// Package branca implements the branca token specification.\npackage branca\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/eknkc/basex\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n)\n\nconst (\n\tversion byte   = 0xBA // Branca magic byte\n\tbase62  string = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n)\n\nvar (\n\t// ErrInvalidToken indicates an invalid token.\n\tErrInvalidToken = errors.New(\"invalid base62 token\")\n\t// ErrInvalidTokenVersion indicates an invalid token version.\n\tErrInvalidTokenVersion = errors.New(\"invalid token version\")\n\t// ErrBadKeyLength indicates a bad key length.\n\tErrBadKeyLength = errors.New(\"bad key length\")\n)\n\n// ErrExpiredToken indicates an expired token.\ntype ErrExpiredToken struct {\n\t// Time is the token expiration time.\n\tTime time.Time\n}\n\nfunc (e *ErrExpiredToken) Error() string {\n\tdelta := time.Unix(time.Now().Unix(), 0).Sub(time.Unix(e.Time.Unix(), 0))\n\treturn fmt.Sprintf(\"token is expired by %v\", delta)\n}\n\n// Branca holds a key of exactly 32 bytes. The nonce and timestamp are used for acceptance tests.\ntype Branca struct {\n\tKey       string\n\tnonce     string\n\tttl       uint32\n\ttimestamp uint32\n}\n\n// SetTTL sets a Time To Live on the token for valid tokens.\nfunc (b *Branca) SetTTL(ttl uint32) {\n\tb.ttl = ttl\n}\n\n// setTimeStamp sets a timestamp for testing.\nfunc (b *Branca) setTimeStamp(timestamp uint32) {\n\tb.timestamp = timestamp\n}\n\n// setNonce sets a nonce for testing.\nfunc (b *Branca) setNonce(nonce string) {\n\tb.nonce = nonce\n}\n\n// NewBranca creates a *Branca struct.\nfunc NewBranca(key string) (b *Branca) {\n\treturn &Branca{\n\t\tKey: key,\n\t}\n}\n\n// EncodeToString encodes the data matching the format:\n// Version (byte) || Timestamp ([4]byte) || Nonce ([24]byte) || Ciphertext ([]byte) || Tag ([16]byte)\nfunc (b *Branca) EncodeToString(data string) (string, error) {\n\tvar timestamp uint32\n\tvar nonce []byte\n\tif b.timestamp == 0 {\n\t\tb.timestamp = uint32(time.Now().Unix())\n\t}\n\ttimestamp = b.timestamp\n\n\tif len(b.nonce) == 0 {\n\t\tnonce = make([]byte, 24)\n\t\tif _, err := rand.Read(nonce); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t} else {\n\t\tnoncebytes, err := hex.DecodeString(b.nonce)\n\t\tif err != nil {\n\t\t\treturn \"\", ErrInvalidToken\n\t\t}\n\t\tnonce = noncebytes\n\t}\n\n\tkey := bytes.NewBufferString(b.Key).Bytes()\n\tpayload := bytes.NewBufferString(data).Bytes()\n\n\ttimeBuffer := make([]byte, 4)\n\tbinary.BigEndian.PutUint32(timeBuffer, timestamp)\n\theader := append(timeBuffer, nonce...)\n\theader = append([]byte{version}, header...)\n\n\txchacha, err := chacha20poly1305.NewX(key)\n\tif err != nil {\n\t\treturn \"\", ErrBadKeyLength\n\t}\n\n\tciphertext := xchacha.Seal(nil, nonce, payload, header)\n\n\ttoken := append(header, ciphertext...)\n\tbase62, err := basex.NewEncoding(base62)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base62.Encode(token), nil\n}\n\n// DecodeToString decodes the data.\nfunc (b *Branca) DecodeToString(data string) (string, error) {\n\tif len(data) < 62 {\n\t\treturn \"\", fmt.Errorf(\"%w: length is less than 62\", ErrInvalidToken)\n\t}\n\tbase62, err := basex.NewEncoding(base62)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%v\", err)\n\t}\n\ttoken, err := base62.Decode(data)\n\tif err != nil {\n\t\treturn \"\", ErrInvalidToken\n\t}\n\theader := token[:29]\n\tciphertext := token[29:]\n\ttokenversion := header[0]\n\ttimestamp := binary.BigEndian.Uint32(header[1:5])\n\tnonce := header[5:]\n\n\tif tokenversion != version {\n\t\treturn \"\", fmt.Errorf(\"%w: got %#X but expected %#X\", ErrInvalidTokenVersion, tokenversion, version)\n\t}\n\n\tkey := bytes.NewBufferString(b.Key).Bytes()\n\n\txchacha, err := chacha20poly1305.NewX(key)\n\tif err != nil {\n\t\treturn \"\", ErrBadKeyLength\n\t}\n\tpayload, err := xchacha.Open(nil, nonce, ciphertext, header)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif b.ttl != 0 {\n\t\tfuture := int64(timestamp + b.ttl)\n\t\tnow := time.Now().Unix()\n\t\tif future < now {\n\t\t\treturn \"\", &ErrExpiredToken{Time: time.Unix(future, 0)}\n\t\t}\n\t}\n\n\tpayloadString := bytes.NewBuffer(payload).String()\n\treturn payloadString, nil\n}\n"
  },
  {
    "path": "branca_test.go",
    "content": "package branca\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\ttestVectors []struct {\n\t\tkey       string\n\t\tnonce     string\n\t\ttimestamp uint32\n\t\tpayload   string\n\t\texpected  string\n\t}\n)\n\n// TestVector1 for testing encoding data to a valid branca token.\nfunc TestVector1(t *testing.T) {\n\ttestVectors = []struct {\n\t\tkey       string\n\t\tnonce     string\n\t\ttimestamp uint32\n\t\tpayload   string\n\t\texpected  string\n\t}{\n\t\t{\"supersecretkeyyoushouldnotcommit\", \"0102030405060708090a0b0c0102030405060708090a0b0c\", 123206400, \"Hello world!\", \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a\"},\n\t}\n\n\tfor _, table := range testVectors {\n\t\tb := NewBranca(table.key)\n\t\tb.setNonce(table.nonce)\n\t\tb.setTimeStamp(table.timestamp)\n\n\t\t// Encode string.\n\t\tencoded, err := b.EncodeToString(table.payload)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%q\", err)\n\t\t}\n\t\tif encoded != table.expected {\n\t\t\tt.Errorf(\"EncodeToString(\\\"%s\\\") = %s. got %s, expected %q\", table.payload, encoded, encoded, table.expected)\n\t\t}\n\n\t\t// Decode string.\n\t\tdecoded, err := b.DecodeToString(encoded)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%q\", err)\n\t\t}\n\t\tif decoded != table.payload {\n\t\t\tt.Errorf(\"DecodeToString(\\\"%s\\\") = %s. got %s, expected %q\", table.expected, decoded, decoded, table.expected)\n\t\t}\n\t}\n}\n\n// TestVector2 for testing encoding data to a valid branca token with a TTL.\nfunc TestVector2(t *testing.T) {\n\ttestVectors = []struct {\n\t\tkey       string\n\t\tnonce     string\n\t\ttimestamp uint32\n\t\tpayload   string\n\t\texpected  string\n\t}{\n\t\t{\"supersecretkeyyoushouldnotcommit\", \"0102030405060708090a0b0c0102030405060708090a0b0c\", 123206400, \"Hello world!\", \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a\"},\n\t}\n\n\tfor _, table := range testVectors {\n\t\tb := NewBranca(table.key)\n\t\tb.setNonce(table.nonce)\n\t\tb.setTimeStamp(table.timestamp)\n\n\t\t// Encode string.\n\t\tencoded, err := b.EncodeToString(table.payload)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%q\", err)\n\t\t}\n\t\tif encoded != table.expected {\n\t\t\tt.Errorf(\"EncodeToString(\\\"%s\\\") = %s. got %s, expected %q\", table.payload, encoded, encoded, table.expected)\n\t\t}\n\n\t\t// Decode string with TTL. Should throw an error with no token encoded because it has expired.\n\t\tb.SetTTL(3600)\n\t\tdecoded, derr := b.DecodeToString(encoded)\n\t\tif derr == nil {\n\t\t\tt.Errorf(\"%q\", derr)\n\t\t}\n\t\tif decoded != \"\" {\n\t\t\tt.Errorf(\"DecodeToString(\\\"%s\\\") = %s. got %s, expected %q\", table.expected, decoded, decoded, table.expected)\n\t\t}\n\t}\n}\n\n// TestGenerateToken for testing issuing branca tokens.\nfunc TestGenerateToken(t *testing.T) {\n\ttestVectors = []struct {\n\t\tkey       string\n\t\tnonce     string\n\t\ttimestamp uint32\n\t\tpayload   string\n\t\texpected  string\n\t}{\n\t\t{\"supersecretkeyyoushouldnotcommit\", \"0102030405060708090a0b0c0102030405060708090a0b0c\", 123206400, \"Hello world!\", \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a\"},\n\t}\n\n\tfor _, table := range testVectors {\n\t\t// Not generated with set timestamp.\n\t\tb := NewBranca(table.key)\n\n\t\t// Encode string.\n\t\tencoded, err := b.EncodeToString(table.payload)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%q\", err)\n\t\t}\n\t\tif encoded == table.expected {\n\t\t\tt.Errorf(\"EncodeToString(\\\"%s\\\") = %s. got %s, expected %q\", table.payload, encoded, encoded, table.expected)\n\t\t}\n\t}\n}\n\n// TestInvalidEncodeString for testing errors when generating branca tokens.\nfunc TestInvalidEncodeString(t *testing.T) {\n\ttestVectors = []struct {\n\t\tkey       string\n\t\tnonce     string\n\t\ttimestamp uint32\n\t\tpayload   string\n\t\texpected  string\n\t}{\n\t\t{\"supersecretkeyyoushouldnotcommi\", \"0102030405060708090a0b0c0102030405060708090a0b0c\", 123206400, \"Hello world!\", \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a\"}, // Invalid key\n\n\t\t{\"supersecretkeyyoushouldnotcommi\", \"\", 123206400, \"Hello world!\",\n\t\t\t\"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a\"}, // Invalid key + no nonce\n\n\t}\n\n\tfor _, table := range testVectors {\n\t\tb := NewBranca(table.key)\n\n\t\t_, err := b.EncodeToString(table.payload)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"%q\", err)\n\t\t}\n\t}\n}\n\n// TestInvalidDecodeString for testing errors when decoding branca tokens.\nfunc TestInvalidDecodeString(t *testing.T) {\n\ttestVectors = []struct {\n\t\tkey       string\n\t\tnonce     string\n\t\ttimestamp uint32\n\t\tpayload   string\n\t\texpected  string\n\t}{\n\t\t{\"supersecretkeyyoushouldnotcommit\", \"0102030405060708090a0b0c0102030405060708090a0b0c\", 123206400, \"Hello world!\", \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0\"}, // Invalid base62\n\n\t\t{\"supersecretkeyyoushouldnotcommi\", \"\", 123206400, \"Hello world!\",\n\t\t\t\"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsA\"}, // Invalid key + Invalid base62.\n\n\t\t{\"supersecretkeyyoushouldnotcommi\", \"0102030405060708090a0b0c0102030405060708090a0b0c\", 123206400, \"Hello world!\", \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a\"}, // Invalid key\n\n\t\t{\"supersecretkeyyoushouldnotcommit\", \"0102030405060708090a0b0c0102030405060708090a0b0c\", 123206400, \"Hello world!\", \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLOZtQ0ekPHt8kJHQp0a\"}, // Invalid malformed base62\n\t}\n\n\tfor _, table := range testVectors {\n\t\tb := NewBranca(table.key)\n\n\t\t_, err := b.DecodeToString(table.expected)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"%q\", err)\n\t\t}\n\t}\n}\n\n// TestExpiredTokenError tests if decoding an expired tokens returns the corresponding error type.\nfunc TestExpiredTokenError(t *testing.T) {\n\tb := NewBranca(\"supersecretkeyyoushouldnotcommit\")\n\n\tttl := time.Second * 1\n\tb.SetTTL(uint32(ttl.Seconds()))\n\ttoken, encErr := b.EncodeToString(\"Hello World!\")\n\tif encErr != nil {\n\t\tt.Errorf(\"%q\", encErr)\n\t}\n\n\t// Wait (with enough additional waiting time) until the token is expired...\n\ttime.Sleep(ttl * 3)\n\t// ...and decode the token again that is expired by now.\n\t_, decErr := b.DecodeToString(token)\n\tvar errExpiredToken *ErrExpiredToken\n\tif !errors.As(decErr, &errExpiredToken) {\n\t\tt.Errorf(\"%v\", decErr)\n\t}\n}\n\n// TestInvalidTokenError tests if decoding an invalid token returns the corresponding error type.\nfunc TestInvalidTokenError(t *testing.T) {\n\tb := NewBranca(\"supersecretkeyyoushouldnotcommit\")\n\n\t_, err := b.DecodeToString(\"$\")\n\tif !errors.Is(err, ErrInvalidToken) {\n\t\tt.Errorf(\"%v\", err)\n\t}\n}\n\n// TestInvalidTokenVersionError tests if decoding an invalid token returns the corresponding error type.\nfunc TestInvalidTokenVersionError(t *testing.T) {\n\t// A token with an invalid version where the HEX value 0XBA has been replaced with 0xFF.\n\t// The original token is \"1WgRcDTWm6MyptVOMG9TeEPVcYW01K6hW5SzLrzCkLlrOOovO5TmpDxQql12N2n0jELx\".\n\ttokenWithInvalidVersion := \"25jsrzc9Q6kmzrnCYWf5Z7LCOG2C7Uiu3NbTP0B9ppLDrxZkhLGOuFVB6FqrWp0ypJTF\"\n\n\tb := NewBranca(\"supersecretkeyyoushouldnotcommit\")\n\t_, err := b.DecodeToString(tokenWithInvalidVersion)\n\tif !errors.Is(err, ErrInvalidTokenVersion) {\n\t\tt.Errorf(\"%v\", err)\n\t}\n}\n\n// TestBadKeyLengthError tests if (en/de)coding a token with an invalid key returns the corresponding error type.\nfunc TestBadKeyLengthError(t *testing.T) {\n\tvalidToken := \"875GH233T7IYrxtgXxlQBYiFobZMQdHAT51vChKsAIYCFxZtL1evV54vYqLyZtQ0ekPHt8kJHQp0a\"\n\ttestKeys := []string{\n\t\t\"\",\n\t\t\"thiskeyistooshort\",\n\t\t\"thiskeyislongerthantheexpected32bytes\",\n\t}\n\n\tfor _, key := range testKeys {\n\t\tb := NewBranca(key)\n\n\t\t_, err := b.DecodeToString(validToken)\n\t\tif !errors.Is(err, ErrBadKeyLength) {\n\t\t\tt.Errorf(\"%v\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/hako/branca\n\ngo 1.13\n\nrequire (\n\tgithub.com/eknkc/basex v1.0.0\n\tgolang.org/x/crypto v0.0.0-20191219195013-becbf705a915\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/eknkc/basex v1.0.0 h1:R2zGRGJAcqEES03GqHU9leUF5n4Pg6ahazPbSTQWCWc=\ngithub.com/eknkc/basex v1.0.0/go.mod h1:k/F/exNEHFdbs3ZHuasoP2E7zeWwZblG84Y7Z59vQRo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\n"
  }
]