├── .travis.yml ├── LICENSE ├── README.md ├── example └── main.go ├── goalone.go ├── goalone_test.go ├── token.go └── token_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.11.x 4 | - 1.12.x 5 | install: 6 | - go get github.com/bwmarrin/go-alone 7 | - go get -v . 8 | - go get -v golang.org/x/lint/golint 9 | script: 10 | - diff <(gofmt -d .) <(echo -n) 11 | - go vet -x ./... 12 | - golint -set_exit_status ./... 13 | - go test -v -race ./... 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Bruce 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | It's dangerous to go-alone! Take this. 2 | ========================================== 3 | [![GoDoc](https://godoc.org/github.com/bwmarrin/go-alone?status.svg)](https://godoc.org/github.com/bwmarrin/go-alone) [![Go report](http://goreportcard.com/badge/bwmarrin/go-alone)](http://goreportcard.com/report/bwmarrin/go-alone) [![Build Status](https://travis-ci.org/bwmarrin/go-alone.svg?branch=master)](https://travis-ci.org/bwmarrin/go-alone) [![Coverage](http://gocover.io/_badge/github.com/bwmarrin/go-alone)](https://gocover.io/github.com/bwmarrin/go-alone) [![Discord Gophers](https://img.shields.io/badge/Discord%20Gophers-%23info-blue.svg)](https://discord.gg/0f1SbxBZjYq9jLBk) 4 | 5 | 6 | 7 | go-alone is a [Go](https://golang.org/) package that provides 8 | * Methods to create and verify [MAC](https://en.wikipedia.org/wiki/Message_authentication_code) signatures of data 9 | * Ability to add timestamps to signed tokens and use custom epoch if needed. 10 | * BLAKE2b signatures and Base58 time encoding provides outstanding performance and security. 11 | * A very simple to use API with good documentation and 100% test coverage. 12 | * Various helper methods for parsing tokens 13 | 14 | **For more information, please read the [wiki](https://github.com/bwmarrin/go-alone/wiki)** 15 | 16 | **For help with this package or general Go discussion, please join the [Discord 17 | Gophers](https://discord.gg/0f1SbxBZjYq9jLBk) chat server.** 18 | 19 | **For a fast and easy to use snowflake ID library, check out [this](https://github.com/bwmarrin/snowflake)** 20 | 21 | ## Getting Started 22 | This assumes you already have a working Go environment, if not please see 23 | [this page](https://golang.org/doc/install) first. 24 | 25 | ### Installing 26 | 27 | 28 | ```sh 29 | go get github.com/bwmarrin/go-alone 30 | ``` 31 | 32 | ### Usage 33 | 34 | Here's a basic example below. There is also an example program in the [example](https://github.com/bwmarrin/go-alone/tree/master/example) 35 | folder that demonstrates a few more ways of using Go-Alone. You can read the API 36 | documentation on [GoDoc](https://godoc.org/github.com/bwmarrin/go-alone). 37 | 38 | ```go 39 | package main 40 | 41 | import ( 42 | "github.com/bwmarrin/go-alone" 43 | ) 44 | 45 | func main() { 46 | 47 | // This secret is used as the hash key for the signer. 48 | var secret = []byte("It's a secret to everybody") 49 | 50 | // This data is what we will be signing below. 51 | var data = []byte("It's dangerous to go alone! Take this.") 52 | 53 | // Create a new Signer using our secret 54 | s := goalone.New(secret) 55 | 56 | // Sign and return a token in the form of `data.signature` 57 | token := s.Sign(data) 58 | 59 | // Unsign the token to verify it - if successful the data portion of the 60 | // token is returned. If unsuccessful then d will be nil, and an error 61 | // is returned. 62 | d, err := s.Unsign(token) 63 | if err != nil { 64 | // signature is not valid. Token was tampered with, forged, or maybe it's 65 | // not even a token at all! Either way, it's not safe to use it. 66 | } else { 67 | // signature is valid, it is safe to use the data 68 | println(string(d)) 69 | } 70 | } 71 | ``` 72 | 73 | 74 | ### Performance / Testing 75 | 76 | To run the tests and benchmarks, use the following command. 77 | 78 | ```sh 79 | go test -bench=. -v 80 | ``` 81 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bwmarrin/go-alone" 7 | ) 8 | 9 | func main() { 10 | 11 | // This secret is used as the hash key for the signer. 12 | var secret = []byte("It's a secret to everybody") 13 | 14 | // This data is what we will be signing below. 15 | var data = []byte("It's dangerous to go alone! Take this.") 16 | 17 | // Create a new Signer using our secret 18 | s := goalone.New(secret) 19 | 20 | // Sign and return a token in the form of `data.signature` 21 | token := s.Sign(data) 22 | 23 | // You can reuse this struct as many times as you wish 24 | token2 := s.Sign(data) 25 | 26 | // You can easily Unsign a token, which will verify the signature is valid 27 | // then return signed data of the token. 28 | data, err := s.Unsign(token) 29 | if err != nil { 30 | // signature is not valid 31 | } else { 32 | // signature is valid, it is safe to use the data 33 | println(string(data)) 34 | } 35 | 36 | // Of course, you can do this as many times as needed as well. 37 | data2, err2 := s.Unsign(token2) 38 | if err2 != nil { 39 | // signature is not valid 40 | } else { 41 | // signature is valid, it is safe to use the data 42 | println(string(data2)) 43 | } 44 | 45 | // You can also write one-liners when you will not be reusing the hash. 46 | token3 := goalone.New(secret).Sign(data) 47 | 48 | // Of course, you can Unsign with a one-liner too. 49 | data3, err3 := goalone.New(secret).Unsign(token3) 50 | if err3 != nil { 51 | // signature is not valid 52 | } else { 53 | // signature is valid, it is safe to use the data 54 | println(string(data3)) 55 | } 56 | 57 | // You can pass functional options to the New() function to customize 58 | // a few things. 59 | 60 | // You can have the signer add timestamp to each token using the 61 | // goalone.Timestamp option. 62 | 63 | // You can set a custom timestamp epoch, if you want. Just give it a 64 | // unix timestamp in seconds and go-alone will use it as an offset for all 65 | // timestamps. This will allow you to better future proof your tokens or to 66 | // just make them more obsecure. 67 | 68 | // Here's an example of passing both the Timestamp option and a custom 69 | // epoch. 70 | s = goalone.New(secret, goalone.Timestamp, goalone.Epoch(1293840000)) 71 | token = s.Sign(data) 72 | 73 | // Of course you can do this all as a one liner too, but it does start to get 74 | // a bit too long :) 75 | goalone.New(secret, goalone.Timestamp, goalone.Epoch(1293840000)).Sign(data) 76 | 77 | // You can parse out a token into a struct that separates the payload and 78 | // timestamp for you. 79 | ts := s.Parse(token) 80 | 81 | // Now, we can print the payload with 82 | println(string(ts.Payload)) 83 | 84 | // We can print the timestamp with 85 | println(ts.Timestamp.String()) 86 | 87 | // We can even check how old our timestamp is. You can do a lot of other 88 | // things with the Timestamp too, since it's a standard time.Time value. 89 | if time.Since(ts.Timestamp) > time.Hour { 90 | // That token's timestamp is over an hour old! 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /goalone.go: -------------------------------------------------------------------------------- 1 | package goalone 2 | 3 | import ( 4 | "crypto/subtle" 5 | "encoding/base64" 6 | "errors" 7 | "hash" 8 | "sync" 9 | "time" 10 | 11 | "golang.org/x/crypto/blake2b" 12 | ) 13 | 14 | // Sword is a magical Wooden Sword to be used for protection, because it's dangerous out 15 | // there... Also, it is the main struct used to sign and unsign data using this 16 | // package. 17 | type Sword struct { 18 | sync.Mutex 19 | hash hash.Hash 20 | dirty bool 21 | timestamp bool 22 | epoch int64 23 | } 24 | 25 | // ErrInvalidSignature is returned by Unsign when the provided token's 26 | // signatuire is not valid. 27 | var ErrInvalidSignature = errors.New("invalid signature") 28 | 29 | // ErrShortToken is returned by Unsign when the provided token's length 30 | // is too short to be a vlaid token. 31 | var ErrShortToken = errors.New("token is too small to be valid") 32 | 33 | // New takes a secret key and returns a new Sword. If no Options are provided 34 | // then minimal defaults will be used. NOTE: The key must be 64 bytes or less 35 | // in size. If a larger key is provided it will be truncated to 64 bytes. 36 | //func New(key []byte, o *Options) *Sword { 37 | func New(key []byte, options ...func(*Sword)) *Sword { 38 | 39 | var err error 40 | 41 | // Create a map for decoding Base58. This speeds up the process tremendously. 42 | for i := 0; i < len(encodeBase58Map); i++ { 43 | decodeBase58Map[encodeBase58Map[i]] = byte(i) 44 | } 45 | 46 | s := &Sword{} 47 | 48 | for _, opt := range options { 49 | opt(s) 50 | } 51 | 52 | s.hash, err = blake2b.New256(key) 53 | if err != nil { 54 | // The only possible error that can be returned here is if the key 55 | // is larger than 64 bytes - which the blake2b hash will not accept. 56 | // This is a case that is so easily avoidable when using this pacakge 57 | // and since chaining is convenient for this package. We're going 58 | // to do the below to handle this possible case so we don't have 59 | // to return an error. 60 | s.hash, _ = blake2b.New256(key[0:64]) 61 | } 62 | 63 | return s 64 | } 65 | 66 | // Epoch is a functional option that can be passed to New() to set the Epoch 67 | // to be used. 68 | func Epoch(e int64) func(*Sword) { 69 | return func(s *Sword) { 70 | s.epoch = e 71 | } 72 | } 73 | 74 | // Timestamp is a functional option that can be passed to New() to add a 75 | // timestamp to signatures. 76 | func Timestamp(s *Sword) { 77 | s.timestamp = true 78 | } 79 | 80 | // Sign signs data and returns []byte in the format `data.signature`. Optionally 81 | // add a timestamp and return in the format `data.timestamp.signature` 82 | func (s *Sword) Sign(data []byte) []byte { 83 | 84 | // Build the payload 85 | el := base64.RawURLEncoding.EncodedLen(s.hash.Size()) 86 | var t []byte 87 | 88 | if s.timestamp { 89 | ts := time.Now().Unix() - s.epoch 90 | etl := encodeBase58Len(ts) 91 | t = make([]byte, 0, len(data)+etl+el+2) // +2 for "." chars 92 | t = append(t, data...) 93 | t = append(t, '.') 94 | t = t[0 : len(t)+etl] // expand for timestamp 95 | encodeBase58(ts, t) 96 | } else { 97 | t = make([]byte, 0, len(data)+el+1) 98 | t = append(t, data...) 99 | } 100 | 101 | // Append and encode signature to token 102 | t = append(t, '.') 103 | tl := len(t) 104 | t = t[0 : tl+el] 105 | 106 | // Add the signature to the token 107 | s.sign(t[tl:], t[0:tl-1]) 108 | 109 | // Return the token to the caller 110 | return t 111 | } 112 | 113 | // Unsign validates a signature and if successful returns the data portion of 114 | // the []byte. If unsuccessful it will return an error and nil for the data. 115 | func (s *Sword) Unsign(token []byte) ([]byte, error) { 116 | 117 | tl := len(token) 118 | el := base64.RawURLEncoding.EncodedLen(s.hash.Size()) 119 | 120 | // A token must be at least el+2 bytes long to be valid. 121 | if tl < el+2 { 122 | return nil, ErrShortToken 123 | } 124 | 125 | // Get the signature of the payload 126 | dst := make([]byte, el) 127 | s.sign(dst, token[0:tl-(el+1)]) 128 | 129 | if subtle.ConstantTimeCompare(token[tl-el:], dst) != 1 { 130 | return nil, ErrInvalidSignature 131 | } 132 | 133 | return token[0 : tl-(el+1)], nil 134 | } 135 | 136 | /////////////////////////////////////////////////////////////////////////////// 137 | // Unexported Code //////////////////////////////////////////////////////////// 138 | 139 | // This is the map of characters used during base58 encoding. These replicate 140 | // the flickr shortid mapping. 141 | const encodeBase58Map = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ" 142 | 143 | // Used to create a decode map so we can decode base58 fairly fast. 144 | var decodeBase58Map [256]byte 145 | 146 | // sign creates the encoded signature of payload and writes to dst 147 | func (s *Sword) sign(dst, payload []byte) { 148 | 149 | s.Lock() 150 | if s.dirty { 151 | s.hash.Reset() 152 | } 153 | s.dirty = true 154 | s.hash.Write(payload) 155 | h := s.hash.Sum(nil) 156 | s.Unlock() 157 | 158 | base64.RawURLEncoding.Encode(dst, h) 159 | } 160 | 161 | // returns the len of base58 encoded i 162 | func encodeBase58Len(i int64) int { 163 | 164 | var l = 1 165 | for i >= 58 { 166 | l++ 167 | i /= 58 168 | } 169 | return l 170 | } 171 | 172 | // encode time int64 into b []byte 173 | func encodeBase58(i int64, b []byte) { 174 | p := len(b) - 1 175 | for i >= 58 { 176 | b[p] = encodeBase58Map[i%58] 177 | p-- 178 | i /= 58 179 | } 180 | b[p] = encodeBase58Map[i] 181 | } 182 | 183 | // parses a base58 []byte into a int64 184 | func decodeBase58(b []byte) int64 { 185 | var id int64 186 | for p := range b { 187 | id = id*58 + int64(decodeBase58Map[b[p]]) 188 | } 189 | return id 190 | } 191 | -------------------------------------------------------------------------------- /goalone_test.go: -------------------------------------------------------------------------------- 1 | package goalone 2 | 3 | import ( 4 | "crypto/subtle" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestTimeCoding(t *testing.T) { 11 | 12 | New(nil) // needed to init the decodemap 13 | 14 | var now int64 15 | for i := 0; i < 41; i++ { 16 | b := make([]byte, encodeBase58Len(now)) 17 | encodeBase58(now, b) 18 | id := decodeBase58(b) 19 | if id != now { 20 | fmt.Printf("Time.Now() : %d\n", now) 21 | fmt.Printf("encodeBase58 : %s\n", b) 22 | fmt.Printf("decodeBase58 : %d\n", id) 23 | t.Fatal("id != now!") 24 | } 25 | now += 1 + now*2 26 | } 27 | } 28 | func TestNewNil(t *testing.T) { 29 | 30 | s := New(nil) 31 | if s == nil { 32 | t.Fatal("New returned a nil") 33 | } 34 | 35 | if s.dirty { 36 | t.Fatal("New returned a dirty hash") 37 | } 38 | } 39 | 40 | func TestNewSecret(t *testing.T) { 41 | 42 | secret := []byte(`B1nzyRateLimits`) 43 | s := New(secret) 44 | if s == nil { 45 | t.Fatal("New returned a nil") 46 | } 47 | 48 | if s.hash == nil { 49 | t.Fatal("New returned a Sword with a nil hash") 50 | } 51 | 52 | if s.dirty { 53 | t.Fatal("New returned a dirty hash") 54 | } 55 | } 56 | 57 | func TestNewSecretTooBig(t *testing.T) { 58 | 59 | secret := []byte(`B1nzyRateLid;flkjasdl;fjasd;lkfjkl;ljasd;fkljsda;fkljasd;klfj;asdjts`) 60 | s := New(secret) 61 | if s == nil { 62 | t.Fatal("New returned a nil") 63 | } 64 | 65 | if s.hash == nil { 66 | t.Fatal("New returned a Sword with a nil hash") 67 | } 68 | 69 | if s.dirty { 70 | t.Fatal("New returned a dirty hash") 71 | } 72 | 73 | } 74 | 75 | func TestUnsignTooLittle(t *testing.T) { 76 | 77 | secret := []byte(`B1nzyRateLimits`) 78 | token := []byte("9yhD.QheV.k0W") 79 | 80 | s := New(secret) 81 | got, err := s.Unsign(token) 82 | 83 | if got != nil { 84 | t.Error("Unsign returned data, but should have returned nil") 85 | } 86 | 87 | if err != ErrShortToken { 88 | t.Fatal("Unsign did not return the correct error") 89 | } 90 | 91 | } 92 | 93 | func TestSign(t *testing.T) { 94 | 95 | secret := []byte(`B1nzyRateLimits`) 96 | data := []byte(`1203981209381290.LutinRocks`) 97 | want := []byte(`1203981209381290.LutinRocks.ZGRsRXvTb08ld7xmJImL1ykGr8D1JmrSPGc134nBNRo`) 98 | 99 | s := New(secret) 100 | token := s.Sign(data) 101 | 102 | if subtle.ConstantTimeCompare(token, want) != 1 { 103 | t.Logf("data: \n%s\n", data) 104 | t.Logf("want: \n%s\n", want) 105 | t.Logf("got : \n%s\n", token) 106 | t.Fatal("token and want do not match") 107 | } 108 | 109 | // sign after a dirty hash 110 | 111 | if !s.dirty { 112 | t.Fatal("Hash is not dirty, but it should be") 113 | } 114 | 115 | token = s.Sign(data) 116 | 117 | if subtle.ConstantTimeCompare(token, want) != 1 { 118 | t.Logf("data: \n%s\n", data) 119 | t.Logf("want: \n%s\n", want) 120 | t.Logf("got : \n%s\n", token) 121 | t.Fatal("token and want do not match") 122 | } 123 | 124 | } 125 | 126 | func TestSignTimestamp(t *testing.T) { 127 | 128 | secret := []byte(`B1nzyRateLimits`) 129 | data := []byte(`1203981209381290.LutinRocks`) 130 | 131 | s := New(secret, Timestamp) 132 | token := s.Sign(data) 133 | 134 | // Make sure we got the same payload 135 | if subtle.ConstantTimeCompare(token[0:27], data) != 1 { 136 | t.Logf("token: \n%s\n", token[0:27]) 137 | t.Logf("want: \n%s\n", data) 138 | t.Fatal("Payload was changed") 139 | } 140 | 141 | // Make sure we got a timestamp.... (not a great test..) 142 | if token[27] != '.' || token[34] != '.' { 143 | t.Fatal("Doesn't appear to be a timestamp.") 144 | } 145 | 146 | } 147 | 148 | func TestSignEpoch(t *testing.T) { 149 | 150 | secret := []byte(`B1nzyRateLimits`) 151 | data := []byte(`1203981209381290.LutinRocks`) 152 | 153 | s := New(secret, Epoch(1293840000), Timestamp) 154 | token := s.Sign(data) 155 | 156 | // Make sure we got the same payload 157 | if subtle.ConstantTimeCompare(token[0:27], data) != 1 { 158 | t.Logf("token: \n%s\n", token[0:27]) 159 | t.Logf("want: \n%s\n", data) 160 | t.Fatal("Payload was changed") 161 | } 162 | 163 | // Make sure we got a timestamp.... (not a great test..) 164 | if token[27] != '.' || token[33] != '.' { 165 | t.Fatal("Doesn't appear to be a timestamp.") 166 | } 167 | 168 | } 169 | 170 | func TestUnsign(t *testing.T) { 171 | 172 | secret := []byte(`B1nzyRateLimits`) 173 | token := []byte(`1203981209381290.LutinRocks.ZGRsRXvTb08ld7xmJImL1ykGr8D1JmrSPGc134nBNRo`) 174 | want := []byte(`1203981209381290.LutinRocks`) 175 | 176 | s := New(secret) 177 | got, err := s.Unsign(token) 178 | if err != nil { 179 | t.Fatal("Unsign returned an err,", err) 180 | } 181 | if subtle.ConstantTimeCompare(got, want) != 1 { 182 | t.Logf("token: \n%s\n", token) 183 | t.Logf("want: \n%s\n", want) 184 | t.Logf("got : \n%s\n", got) 185 | t.Fatal("data of token does not match original data") 186 | } 187 | 188 | if !s.dirty { 189 | t.Fatal("Hash is not dirty, but it should be") 190 | } 191 | 192 | token = []byte(`1203981209381290.LutinRocks.ZGRsRXvTb08ld7xmJImL1ykGr8D1JmrSPGc134nBNR0`) 193 | got, err = s.Unsign(token) 194 | if err != ErrInvalidSignature { 195 | t.Fatal("Unsign returned incorrect error") 196 | } 197 | if got != nil { 198 | t.Logf("token: \n%s\n", token) 199 | t.Logf("want: \n%s\n", want) 200 | t.Logf("got : \n%s\n", got) 201 | t.Fatal("got should be nil") 202 | } 203 | } 204 | 205 | func BenchmarkNew(b *testing.B) { 206 | secret := []byte(`B1nzyRateLimits`) 207 | 208 | b.ReportAllocs() 209 | b.ResetTimer() 210 | 211 | for n := 0; n < b.N; n++ { 212 | New(secret) 213 | } 214 | } 215 | 216 | func BenchmarkSignLittle(b *testing.B) { 217 | secret := []byte(`B1nzyRateLimits`) 218 | data := []byte(`1203981209381290.LutinRocks`) 219 | 220 | b.ReportAllocs() 221 | b.ResetTimer() 222 | 223 | for n := 0; n < b.N; n++ { 224 | New(secret).Sign(data) 225 | } 226 | } 227 | 228 | func BenchmarkReuseSignLittle(b *testing.B) { 229 | s := New([]byte(`B1nzyRateLimits`)) 230 | data := []byte(`1203981209381290.LutinRocks`) 231 | 232 | b.ReportAllocs() 233 | b.ResetTimer() 234 | 235 | for n := 0; n < b.N; n++ { 236 | s.Sign(data) 237 | } 238 | } 239 | 240 | func BenchmarkReuseSignTimestampLittle(b *testing.B) { 241 | s := New([]byte(`B1nzyRateLimits`), Timestamp) 242 | data := []byte(`1203981209381290.LutinRocks`) 243 | 244 | b.ReportAllocs() 245 | b.ResetTimer() 246 | 247 | for n := 0; n < b.N; n++ { 248 | s.Sign(data) 249 | } 250 | } 251 | 252 | func BenchmarkSignBig(b *testing.B) { 253 | secret := []byte(`B1nzyRateLimits`) 254 | data := []byte(`1203981209381290.7h90g7h089234g75908347gh09384h7v0897fg08947f5097423058974h908fg702f9j75028fg5704239hg7053498dj7249038jd57j097g5v029dh79hc47f507v9082h7f509234j7dc02d750j24935h7f924`) 255 | 256 | b.ReportAllocs() 257 | b.ResetTimer() 258 | 259 | for n := 0; n < b.N; n++ { 260 | New(secret).Sign(data) 261 | } 262 | } 263 | 264 | func BenchmarkSignBigReuse(b *testing.B) { 265 | s := New([]byte(`B1nzyRateLimits`)) 266 | data := []byte(`1203981209381290.7h90g7h089234g75908347gh09384h7v0897fg08947f5097423058974h908fg702f9j75028fg5704239hg7053498dj7249038jd57j097g5v029dh79hc47f507v9082h7f509234j7dc02d750j24935h7f924`) 267 | 268 | b.ReportAllocs() 269 | b.ResetTimer() 270 | 271 | for n := 0; n < b.N; n++ { 272 | s.Sign(data) 273 | } 274 | } 275 | func BenchmarkUnsignLittle(b *testing.B) { 276 | secret := []byte(`B1nzyRateLimits`) 277 | data := []byte(`1203981209381290.LutinRocks`) 278 | 279 | s := New([]byte(`B1nzyRateLimits`)) 280 | t := s.Sign(data) 281 | 282 | b.ReportAllocs() 283 | b.ResetTimer() 284 | 285 | for n := 0; n < b.N; n++ { 286 | New(secret).Unsign(t) 287 | } 288 | } 289 | 290 | func BenchmarkUnsignLittleReuse(b *testing.B) { 291 | s := New([]byte(`B1nzyRateLimits`)) 292 | data := []byte(`1203981209381290.LutinRocks`) 293 | t := s.Sign(data) 294 | 295 | b.ReportAllocs() 296 | b.ResetTimer() 297 | 298 | for n := 0; n < b.N; n++ { 299 | s.Unsign(t) 300 | } 301 | } 302 | 303 | func BenchmarkUnsignBig(b *testing.B) { 304 | secret := []byte(`B1nzyRateLimits`) 305 | data := []byte(`1203981209381290.7h90g7h089234g75908347gh09384h7v0897fg08947f5097423058974h908fg702f9j75028fg5704239hg7053498dj7249038jd57j097g5v029dh79hc47f507v9082h7f509234j7dc02d750j24935h7f924`) 306 | s := New([]byte(`B1nzyRateLimits`)) 307 | t := s.Sign(data) 308 | 309 | b.ReportAllocs() 310 | b.ResetTimer() 311 | 312 | for n := 0; n < b.N; n++ { 313 | New(secret).Unsign(t) 314 | } 315 | } 316 | 317 | func BenchmarkUnsignReuseBig(b *testing.B) { 318 | s := New([]byte(`B1nzyRateLimits`)) 319 | data := []byte(`1203981209381290.7h90g7h089234g75908347gh09384h7v0897fg08947f5097423058974h908fg702f9j75028fg5704239hg7053498dj7249038jd57j097g5v029dh79hc47f507v9082h7f509234j7dc02d750j24935h7f924`) 320 | t := s.Sign(data) 321 | 322 | b.ReportAllocs() 323 | b.ResetTimer() 324 | 325 | for n := 0; n < b.N; n++ { 326 | s.Unsign(t) 327 | } 328 | } 329 | 330 | func BenchmarkEncodeBase58(b *testing.B) { 331 | 332 | now := time.Now().Unix() 333 | by := make([]byte, 6) 334 | 335 | b.ReportAllocs() 336 | b.ResetTimer() 337 | for n := 0; n < b.N; n++ { 338 | encodeBase58(now, by) 339 | } 340 | } 341 | 342 | func BenchmarkDecodeBase58(b *testing.B) { 343 | 344 | now := time.Now().Unix() 345 | by := make([]byte, 6) 346 | encodeBase58(now, by) 347 | 348 | b.ReportAllocs() 349 | b.ResetTimer() 350 | for n := 0; n < b.N; n++ { 351 | decodeBase58([]byte(by)) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | package goalone 2 | 3 | import ( 4 | "encoding/base64" 5 | "time" 6 | ) 7 | 8 | // Token is used to parse out a []byte token provided by Sign() 9 | type Token struct { 10 | Payload []byte 11 | Timestamp time.Time 12 | } 13 | 14 | // Parse will parse the []byte token returned from Sign based on the Sword 15 | // Options into a Token struct. For this to work corectly the Sword Options need 16 | // to match that of what was used when the token was initially created. 17 | func (s *Sword) Parse(t []byte) Token { 18 | 19 | tl := len(t) 20 | el := base64.RawURLEncoding.EncodedLen(s.hash.Size()) 21 | 22 | token := Token{} 23 | 24 | if s.timestamp { 25 | // we need to find out how many bytes the timestamp is. 26 | // so lets start at the start of the hash, and work back looking for our 27 | // separator - XXX: I'm sure there's room for improvement here. 28 | for i := tl - (el + 2); i >= 0; i-- { 29 | if t[i] == '.' { 30 | token.Payload = t[0:i] 31 | token.Timestamp = time.Unix(decodeBase58(t[i+1:tl-(el+1)])+s.epoch, 0) 32 | break 33 | } 34 | } 35 | } else { 36 | token.Payload = t[0 : tl-(el+1)] 37 | } 38 | 39 | return token 40 | } 41 | -------------------------------------------------------------------------------- /token_test.go: -------------------------------------------------------------------------------- 1 | package goalone 2 | 3 | import ( 4 | "crypto/subtle" 5 | "testing" 6 | ) 7 | 8 | func TestParse(t *testing.T) { 9 | 10 | secret := []byte(`B1nzyRateLimits`) 11 | want := []byte(`1203981209381290.LutinRocks`) 12 | bt := []byte(`1203981209381290.LutinRocks.ZGRsRXvTb08ld7xmJImL1ykGr8D1JmrSPGc134nBNRo`) 13 | 14 | s := New(secret) 15 | token := s.Parse(bt) 16 | 17 | if subtle.ConstantTimeCompare(token.Payload, want) != 1 { 18 | t.Logf("Got %s\n", token.Payload) 19 | t.Logf("Want %s\n", want) 20 | t.Fatal("token and want do not match") 21 | } 22 | 23 | if !token.Timestamp.IsZero() { 24 | t.Fatal("Timestamp parsed incorrectly") 25 | } 26 | } 27 | 28 | func TestParseTimestamp(t *testing.T) { 29 | 30 | secret := []byte(`B1nzyRateLimits`) 31 | bt := []byte(`1203981209381290.LutinRocks.3gteYe.ZGRsRXvTb08ld7xmJImL1ykGr8D1JmrSPGc134nBNRo`) 32 | want := []byte(`1203981209381290.LutinRocks`) 33 | 34 | s := New(secret, Timestamp) 35 | token := s.Parse(bt) 36 | 37 | if subtle.ConstantTimeCompare(token.Payload, want) != 1 { 38 | t.Logf("Got %s\n", token.Payload) 39 | t.Logf("Want %s\n", want) 40 | t.Fatal("token and want do not match") 41 | } 42 | 43 | if token.Timestamp.Unix() != 1487775993 { 44 | t.Logf("Got %d\n", token.Timestamp.Unix()) 45 | t.Logf("Want %s\n", want) 46 | t.Fatal("Timestamp parsed incorrectly") 47 | } 48 | } 49 | 50 | func BenchmarkParse(b *testing.B) { 51 | 52 | bt := []byte(`1203981209381290.LutinRocks.ZGRsRXvTb08ld7xmJImL1ykGr8D1JmrSPGc134nBNRo`) 53 | secret := []byte(`B1nzyRateLimits`) 54 | s := New(secret) 55 | 56 | b.ReportAllocs() 57 | b.ResetTimer() 58 | for n := 0; n < b.N; n++ { 59 | s.Parse(bt) 60 | } 61 | } 62 | 63 | func BenchmarkParseTimestamp(b *testing.B) { 64 | 65 | bt := []byte(`1203981209381290.LutinRocks.3gteYe.ZGRsRXvTb08ld7xmJImL1ykGr8D1JmrSPGc134nBNRo`) 66 | secret := []byte(`B1nzyRateLimits`) 67 | s := New(secret, Timestamp) 68 | 69 | b.ReportAllocs() 70 | b.ResetTimer() 71 | for n := 0; n < b.N; n++ { 72 | s.Parse(bt) 73 | } 74 | } 75 | --------------------------------------------------------------------------------