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
[](https://travis-ci.org/hako/branca) [](https://goreportcard.com/report/github.com/hako/branca)
[](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=
gitextract_coouietz/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── branca.go ├── branca_test.go ├── go.mod └── go.sum
SYMBOL INDEX (20 symbols across 2 files)
FILE: branca.go
constant version (line 18) | version byte = 0xBA
constant base62 (line 19) | base62 string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
type ErrExpiredToken (line 32) | type ErrExpiredToken struct
method Error (line 37) | func (e *ErrExpiredToken) Error() string {
type Branca (line 43) | type Branca struct
method SetTTL (line 51) | func (b *Branca) SetTTL(ttl uint32) {
method setTimeStamp (line 56) | func (b *Branca) setTimeStamp(timestamp uint32) {
method setNonce (line 61) | func (b *Branca) setNonce(nonce string) {
method EncodeToString (line 74) | func (b *Branca) EncodeToString(data string) (string, error) {
method DecodeToString (line 119) | func (b *Branca) DecodeToString(data string) (string, error) {
function NewBranca (line 66) | func NewBranca(key string) (b *Branca) {
FILE: branca_test.go
function TestVector1 (line 20) | func TestVector1(t *testing.T) {
function TestVector2 (line 57) | func TestVector2(t *testing.T) {
function TestGenerateToken (line 95) | func TestGenerateToken(t *testing.T) {
function TestInvalidEncodeString (line 122) | func TestInvalidEncodeString(t *testing.T) {
function TestInvalidDecodeString (line 148) | func TestInvalidDecodeString(t *testing.T) {
function TestExpiredTokenError (line 177) | func TestExpiredTokenError(t *testing.T) {
function TestInvalidTokenError (line 198) | func TestInvalidTokenError(t *testing.T) {
function TestInvalidTokenVersionError (line 208) | func TestInvalidTokenVersionError(t *testing.T) {
function TestBadKeyLengthError (line 221) | func TestBadKeyLengthError(t *testing.T) {
Condensed preview — 8 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (17K chars).
[
{
"path": ".gitignore",
"chars": 266,
"preview": "# 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 spe"
},
{
"path": ".travis.yml",
"chars": 48,
"preview": "language: go\n\ngo:\n - \"1.13\"\n - \"1.14\"\n - tip\n"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "\nThe MIT License (MIT)\n\nCopyright (c) 2018 Wesley Hill\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 2054,
"preview": "# branca\n\n[](https://travis-ci.org/hako/branca) [![G"
},
{
"path": "branca.go",
"chars": 3993,
"preview": "// Package branca implements the branca token specification.\npackage branca\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding"
},
{
"path": "branca_test.go",
"chars": 7380,
"preview": "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 st"
},
{
"path": "go.mod",
"chars": 139,
"preview": "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-20191219195"
},
{
"path": "go.sum",
"chars": 987,
"preview": "github.com/eknkc/basex v1.0.0 h1:R2zGRGJAcqEES03GqHU9leUF5n4Pg6ahazPbSTQWCWc=\ngithub.com/eknkc/basex v1.0.0/go.mod h1:k/"
}
]
About this extraction
This page contains the full source code of the hako/branca GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 8 files (15.6 KB), approximately 5.3k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.