diff --git a/README.md b/README.md index 55edf6a..cdfbbd1 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ This go library exposed as `zntr.io/paseto` provides : > This is used in my OIDC framework [SolID](https://github.com/zntrio/solid). +I removed the JSON part encoding requirement to allow PASETO to be used as a +generic data container. You can still use JSON, but also more bytes oriented +serialization for `message`, `footer` and `implicit-assertion`. + ## What is PASETO? From https://github.com/paragonie/paseto : @@ -32,7 +36,7 @@ func main () { m := []byte("my super secret message") // Encrypt the token - token, err := pasetov4.Encrypt(rand.Reader, localKey, m, "", "") + token, err := pasetov4.Encrypt(rand.Reader, localKey, m, nil, nil) if err != nil { panic(err) } @@ -43,36 +47,36 @@ More examples - [here](example_test.go) ## Benchmarks +> Go version 1.19.5 / Mac M1 + ### V3 ```sh -$ go test -bench=. -test.benchtime=1s +❯ go test -bench=. -test.benchtime=1s goos: darwin -goarch: amd64 +goarch: arm64 pkg: zntr.io/paseto/v3 -cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz -Benchmark_Paseto_Encrypt-16 95533 12378 ns/op 9616 B/op 78 allocs/op -Benchmark_Paseto_Decrypt-16 108386 11064 ns/op 8488 B/op 71 allocs/op -Benchmark_Paseto_Sign-16 360 3370568 ns/op 1718026 B/op 14190 allocs/op -Benchmark_Paseto_Verify-16 186 6457398 ns/op 3476564 B/op 28712 allocs/op +Benchmark_Paseto_Encrypt-10 74833 14775 ns/op 8274 B/op 59 allocs/op +Benchmark_Paseto_Decrypt-10 84738 14189 ns/op 8050 B/op 59 allocs/op +Benchmark_Paseto_Sign-10 7467 157376 ns/op 9059 B/op 86 allocs/op +Benchmark_Paseto_Verify-10 1980 604653 ns/op 3754 B/op 52 allocs/op PASS -ok zntr.io/paseto/v3 6.361s +ok zntr.io/paseto/v3 5.373s ``` ### V4 ```sh -$ go test -bench=. -test.benchtime=1s +❯ go test -bench=. -test.benchtime=1s goos: darwin -goarch: amd64 +goarch: arm64 pkg: zntr.io/paseto/v4 -cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz -Benchmark_Paseto_Encrypt-16 317936 3437 ns/op 3536 B/op 29 allocs/op -Benchmark_Paseto_Decrypt-16 459136 2484 ns/op 2448 B/op 22 allocs/op -Benchmark_Paseto_Sign-16 51328 23316 ns/op 1672 B/op 18 allocs/op -Benchmark_Paseto_Verify-16 22741 52872 ns/op 744 B/op 13 allocs/op +Benchmark_Paseto_Encrypt-10 461580 2580 ns/op 2288 B/op 12 allocs/op +Benchmark_Paseto_Decrypt-10 554426 2139 ns/op 2064 B/op 12 allocs/op +Benchmark_Paseto_Sign-10 47422 24875 ns/op 928 B/op 4 allocs/op +Benchmark_Paseto_Verify-10 22990 52357 ns/op 704 B/op 4 allocs/op PASS -ok zntr.io/paseto/v4 5.624s +ok zntr.io/paseto/v4 6.660s ``` ## License diff --git a/example_test.go b/example_test.go index 4a82296..9a7401e 100644 --- a/example_test.go +++ b/example_test.go @@ -39,7 +39,7 @@ func ExamplePasetoV4LocalWithoutFooter() { m := []byte("my super secret message") // Encrypt the token - token, err := pasetov4.Encrypt(deterministicSeedForTest, localKey, m, "", "") + token, err := pasetov4.Encrypt(deterministicSeedForTest, localKey, m, nil, nil) if err != nil { panic(err) } @@ -63,10 +63,10 @@ func ExamplePasetoV4LocalWithFooter() { // The footer is public and not encrypted but protected by integrity check. // You can use it to transport information about the token context. - footer := `{"kid":"1234567890"}` + footer := []byte(`{"kid":"1234567890"}`) // Encrypt the token - token, err := pasetov4.Encrypt(deterministicSeedForTest, localKey, m, footer, "") + token, err := pasetov4.Encrypt(deterministicSeedForTest, localKey, m, footer, nil) if err != nil { panic(err) } @@ -87,11 +87,11 @@ func ExamplePasetoV4LocalWithFooterAndImplicitAssertions() { // Prepare the message m := []byte("my super secret message") - footer := `{"kid":"1234567890"}` + footer := []byte(`{"kid":"1234567890"}`) // Assertions are informations not published in the token but kept by the producer // and used during the token integrity check. - assertions := `{"user_id":"1234567890"}` + assertions := []byte(`{"user_id":"1234567890"}`) // Encrypt the token token, err := pasetov4.Encrypt(deterministicSeedForTest, localKey, m, footer, assertions) @@ -114,14 +114,14 @@ func ExamplePasetoV4LocalDecrypt() { } // Encrypted token. - input := []byte("v4.local.dGVzdHMtMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTZ-qF7cj1LApZxpU5R2qdaX9Ox9NaKxnci6ObPVawSbAlqcRdmSDrklvbUqNGk61-tuOKJ0vkFQ.eyJraWQiOiIxMjM0NTY3ODkwIn0") + input := "v4.local.dGVzdHMtMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTZ-qF7cj1LApZxpU5R2qdaX9Ox9NaKxnci6ObPVawSbAlqcRdmSDrklvbUqNGk61-tuOKJ0vkFQ.eyJraWQiOiIxMjM0NTY3ODkwIn0" // Expected footer value. - footer := `{"kid":"1234567890"}` + footer := []byte(`{"kid":"1234567890"}`) // Assertions are informations not published in the token but kept by the producer // and used during the token integrity check. - assertions := `{"user_id":"1234567890"}` + assertions := []byte(`{"user_id":"1234567890"}`) m, err := pasetov4.Decrypt(localKey, input, footer, assertions) if err != nil { @@ -145,8 +145,8 @@ func ExamplePasetoV4PublicSign() { // Prepare the message m := []byte("my super secret message") - footer := `{"kid":"1234567890"}` - assertions := `{"user_id":"1234567890"}` + footer := []byte(`{"kid":"1234567890"}`) + assertions := []byte(`{"user_id":"1234567890"}`) // Sign the token token, err := pasetov4.Sign(m, sk, footer, assertions) @@ -169,9 +169,9 @@ func ExamplePasetoV4PublicVerify() { } // Prepare the message - input := []byte("v4.public.bXkgc3VwZXIgc2VjcmV0IG1lc3NhZ2UbOO-zu6XQbbhmDj0IUEjrmLS_TK1vM69D3pmdbUJmSa7A4c0qjEi9q-DQiMD6UUtbGEMXA1z9zdRskpGfStQH.eyJraWQiOiIxMjM0NTY3ODkwIn0") - footer := `{"kid":"1234567890"}` - assertions := `{"user_id":"1234567890"}` + input := "v4.public.bXkgc3VwZXIgc2VjcmV0IG1lc3NhZ2UbOO-zu6XQbbhmDj0IUEjrmLS_TK1vM69D3pmdbUJmSa7A4c0qjEi9q-DQiMD6UUtbGEMXA1z9zdRskpGfStQH.eyJraWQiOiIxMjM0NTY3ODkwIn0" + footer := []byte(`{"kid":"1234567890"}`) + assertions := []byte(`{"user_id":"1234567890"}`) // Sign the token m, err := pasetov4.Verify(input, pk, footer, assertions) diff --git a/go.mod b/go.mod index 8bf5bfa..aeee5db 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module zntr.io/paseto -go 1.18 +go 1.19 require ( - github.com/stretchr/testify v1.7.1 - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e + github.com/stretchr/testify v1.8.1 + golang.org/x/crypto v0.5.0 ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/sys v0.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9031993..3d7d543 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,19 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/common/common.go b/internal/common/common.go index d5230d5..19d5dff 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -18,47 +18,36 @@ package common import ( - "bytes" - "crypto/subtle" "encoding/binary" ) // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding -func PreAuthenticationEncoding(pieces ...[]byte) ([]byte, error) { - output := &bytes.Buffer{} +func PreAuthenticationEncoding(pieces ...[]byte) []byte { + // Precompute length to allocate the buffer + // PieceCount (8B) || ( PieceLen (8B) || Piece (*B) )* + bufLen := 8 + for i := range pieces { + bufLen += 8 + len(pieces[i]) + } + + // Pre-allocate the buffer + output := make([]byte, bufLen) // Encode piece count - count := len(pieces) - if err := binary.Write(output, binary.LittleEndian, uint64(count)); err != nil { - return nil, err - } + binary.LittleEndian.PutUint64(output, uint64(len(pieces))) + offset := 8 // For each element for i := range pieces { // Encode size - if err := binary.Write(output, binary.LittleEndian, uint64(len(pieces[i]))); err != nil { - return nil, err - } + binary.LittleEndian.PutUint64(output[offset:], uint64(len(pieces[i]))) + offset += 8 // Encode data - if _, err := output.Write(pieces[i]); err != nil { - return nil, err - } + copy(output[offset:], pieces[i]) + offset += len(pieces[i]) } // No error - return output.Bytes(), nil -} - -// SecureCompare use constant time function to compare the two given array. -func SecureCompare(given, actual []byte) bool { - if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { - return subtle.ConstantTimeCompare(given, actual) == 1 - } - // Securely compare actual to itself to keep constant time, but always return false - if subtle.ConstantTimeCompare(actual, actual) == 1 { - return false - } - - return false + return output } diff --git a/internal/common/common_test.go b/internal/common/common_test.go index 19a458d..f325670 100644 --- a/internal/common/common_test.go +++ b/internal/common/common_test.go @@ -27,18 +27,16 @@ func TestPreAuthenticationEncoding(t *testing.T) { pieces [][]byte } tests := []struct { - name string - args args - want []byte - wantErr bool + name string + args args + want []byte }{ { name: "empty", args: args{ pieces: nil, }, - wantErr: false, - want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, }, { name: "one", @@ -47,7 +45,6 @@ func TestPreAuthenticationEncoding(t *testing.T) { []byte("test"), }, }, - wantErr: false, want: []byte{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Count 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Length @@ -57,66 +54,10 @@ func TestPreAuthenticationEncoding(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := PreAuthenticationEncoding(tt.args.pieces...) - if (err != nil) != tt.wantErr { - t.Errorf("PreAuthenticationEncoding() error = %v, wantErr %v", err, tt.wantErr) - return - } + got := PreAuthenticationEncoding(tt.args.pieces...) if !reflect.DeepEqual(got, tt.want) { t.Errorf("PreAuthenticationEncoding() = %v, want %v", got, tt.want) } }) } } - -func TestSecureCompare(t *testing.T) { - type args struct { - given []byte - actual []byte - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "not equal, same size", - args: args{ - given: []byte{0x01}, - actual: []byte{0x02}, - }, - want: false, - }, - { - name: "not equal, different size", - args: args{ - given: []byte{0x01, 0x02}, - actual: []byte{0x02}, - }, - want: false, - }, - { - name: "equal, different size", - args: args{ - given: []byte{0x00}, - actual: []byte{}, - }, - want: false, - }, - { - name: "equal, same size", - args: args{ - given: []byte{0x01}, - actual: []byte{0x01}, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := SecureCompare(tt.args.given, tt.args.actual); got != tt.want { - t.Errorf("SecureCompare() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/v3/helpers.go b/v3/helpers.go index 872c39f..b525e6b 100644 --- a/v3/helpers.go +++ b/v3/helpers.go @@ -61,12 +61,9 @@ func kdf(key *LocalKey, n []byte) (ek, n2, ak []byte, err error) { return ek, n2, ak, nil } -func mac(ak []byte, h string, n, c []byte, f, i string) ([]byte, error) { +func mac(ak, h, n, c, f, i []byte) ([]byte, error) { // Compute pre-authentication message - preAuth, err := common.PreAuthenticationEncoding([]byte(h), n, c, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to compute pre-authentication content: %w", err) - } + preAuth := common.PreAuthenticationEncoding([]byte(h), n, c, f, i) // Compute MAC mac := hmac.New(sha512.New384, ak) diff --git a/v3/local.go b/v3/local.go index 051af68..e4d5fca 100644 --- a/v3/local.go +++ b/v3/local.go @@ -21,12 +21,11 @@ import ( "bytes" "crypto/aes" "crypto/cipher" + "crypto/subtle" "encoding/base64" "errors" "fmt" "io" - - "zntr.io/paseto/internal/common" ) // GenerateLocalKey generates a key for local encryption. @@ -57,72 +56,74 @@ func LocalKeyFromSeed(seed []byte) (*LocalKey, error) { // PASETO v3 symmetric encryption primitive. // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version3.md#encrypt -func Encrypt(r io.Reader, key *LocalKey, m []byte, f, i string) ([]byte, error) { +func Encrypt(r io.Reader, key *LocalKey, m, f, i []byte) (string, error) { // Check arguments if key == nil { - return nil, errors.New("paseto: key is nil") + return "", errors.New("paseto: key is nil") } if len(key) != KeyLength { - return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) + return "", fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) } + // Pre-allocate body + body := make([]byte, nonceLength+len(m), nonceLength+len(m)+macLength) + // Create random seed - var n [nonceLength]byte - if _, err := io.ReadFull(r, n[:]); err != nil { - return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err) + if _, err := io.ReadFull(r, body[:nonceLength]); err != nil { + return "", fmt.Errorf("paseto: unable to generate random seed: %w", err) } // Derive keys from seed and secret key - ek, n2, ak, err := kdf(key, n[:]) + ek, n2, ak, err := kdf(key, body[:nonceLength]) if err != nil { - return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err) + return "", fmt.Errorf("paseto: unable to derive keys from seed: %w", err) } // Prepare an AES-256-CTR stream cipher block, err := aes.NewCipher(ek) if err != nil { - return nil, fmt.Errorf("paseto: unable to prepare block cipher: %w", err) + return "", fmt.Errorf("paseto: unable to prepare block cipher: %w", err) } ciph := cipher.NewCTR(block, n2) // Encrypt the payload - c := make([]byte, len(m)) - ciph.XORKeyStream(c, m) + ciph.XORKeyStream(body[nonceLength:], m) // Compute MAC - t, err := mac(ak, LocalPrefix, n[:], c, f, i) + t, err := mac(ak, []byte(LocalPrefix), body[:nonceLength], body[nonceLength:], f, i) if err != nil { - return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) + return "", fmt.Errorf("paseto: unable to compute MAC: %w", err) } // Serialize final token // h || base64url(n || c || t) - body := append([]byte{}, n[:]...) - body = append(body, c...) body = append(body, t...) // Encode body as RawURLBase64 - encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) - base64.RawURLEncoding.Encode(encodedBody, body) + tokenLen := base64.RawURLEncoding.EncodedLen(len(body)) + footerLen := base64.RawURLEncoding.EncodedLen(len(f)) + 1 + if len(f) > 0 { + tokenLen += base64.RawURLEncoding.EncodedLen(len(f)) + 1 + } + + final := make([]byte, 9+tokenLen) + copy(final, []byte(LocalPrefix)) + base64.RawURLEncoding.Encode(final[9:], body) // Assemble final token - final := append([]byte(LocalPrefix), encodedBody...) - if f != "" { + if len(f) > 0 { + final[9+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) - base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) - - // Assemble body and footer - final = append(final, append([]byte("."), encodedFooter...)...) + base64.RawURLEncoding.Encode(final[9+tokenLen-footerLen+1:], []byte(f)) } // No error - return final, nil + return string(final), nil } // PASETO v3 symmetric decryption primitive // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version3.md#decrypt -func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { +func Decrypt(key *LocalKey, token string, f, i []byte) ([]byte, error) { // Check arguments if key == nil { return nil, errors.New("paseto: key is nil") @@ -130,44 +131,46 @@ func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { if len(key) != KeyLength { return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) } - if input == nil { - return nil, errors.New("paseto: input is nil") + if token == "" { + return nil, errors.New("paseto: token is blank") } + rawToken := []byte(token) + // Check token header - if !bytes.HasPrefix(input, []byte(LocalPrefix)) { + if !bytes.HasPrefix(rawToken, []byte(LocalPrefix)) { return nil, errors.New("paseto: invalid token") } // Trim prefix - input = input[len(LocalPrefix):] + rawToken = rawToken[len(LocalPrefix):] // Check footer usage - if f != "" { + if len(f) > 0 { // Split the footer and the body - parts := bytes.SplitN(input, []byte("."), 2) - if len(parts) != 2 { + footerIdx := bytes.Index(rawToken, []byte(".")) + if footerIdx == 0 { return nil, errors.New("paseto: invalid token, footer is missing but expected") } // Decode footer - footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) - if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { + footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken[footerIdx+1:]))) + if _, err := base64.RawURLEncoding.Decode(footer, rawToken[footerIdx+1:]); err != nil { return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) } // Compare footer - if !common.SecureCompare([]byte(f), footer) { + if subtle.ConstantTimeCompare(f, footer) == 0 { return nil, errors.New("paseto: invalid token, footer mismatch") } // Continue without footer - input = parts[0] + rawToken = rawToken[:footerIdx] } // Decode token - raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(input))) - if _, err := base64.RawURLEncoding.Decode(raw, input); err != nil { + raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken))) + if _, err := base64.RawURLEncoding.Decode(raw, rawToken); err != nil { return nil, fmt.Errorf("paseto: invalid token body: %w", err) } @@ -183,13 +186,13 @@ func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { } // Compute MAC - t2, err := mac(ak, LocalPrefix, n, c, f, i) + t2, err := mac(ak, []byte(LocalPrefix), n, c, f, i) if err != nil { return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) } // Time-constant compare MAC - if !common.SecureCompare(t, t2) { + if subtle.ConstantTimeCompare(t, t2) == 0 { return nil, errors.New("paseto: invalid pre-authentication header") } @@ -201,9 +204,8 @@ func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { ciph := cipher.NewCTR(block, n2) // Decrypt the payload - m := make([]byte, len(c)) - ciph.XORKeyStream(m, c) + ciph.XORKeyStream(c, c) // No error - return m, nil + return c, nil } diff --git a/v3/local_test.go b/v3/local_test.go index ee4a430..b48d417 100644 --- a/v3/local_test.go +++ b/v3/local_test.go @@ -34,9 +34,9 @@ func Test_Paseto_LocalVector(t *testing.T) { key string nonce string token string - payload string - footer string - implicitAssertion string + payload []byte + footer []byte + implicitAssertion []byte }{ { name: "3-E-1", @@ -44,9 +44,9 @@ func Test_Paseto_LocalVector(t *testing.T) { key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", nonce: "0000000000000000000000000000000000000000000000000000000000000000", token: "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeg", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { name: "3-E-2", @@ -54,9 +54,9 @@ func Test_Paseto_LocalVector(t *testing.T) { key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", nonce: "0000000000000000000000000000000000000000000000000000000000000000", token: "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAqhWxBMDgyBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9IzZfaZpReVpHlDSwfuygx1riVXYVs-UjcrG_apl9oz3jCVmmJbRuKn5ZfD8mHz2db0A", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { @@ -65,9 +65,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlxnt5xyhQjFJomwnt7WW_7r2VT0G704ifult011-TgLCyQ2X8imQhniG_hAQ4BydM", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { name: "3-E-4", @@ -75,9 +75,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlBZa_gOpVj4gv0M9lV6Pwjp8JS_MmaZaTA1LLTULXybOBZ2S4xMbYqYmDRhh3IgEk", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { name: "3-E-5", @@ -85,9 +85,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}"), + implicitAssertion: []byte(""), }, { name: "3-E-6", @@ -95,9 +95,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmSeEMphEWHiwtDKJftg41O1F8Hat-8kQ82ZIAMFqkx9q5VkWlxZke9ZzMBbb3Znfo.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}"), + implicitAssertion: []byte(""), }, { name: "3-E-7", @@ -105,9 +105,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJkzWACWAIoVa0bz7EWSBoTEnS8MvGBYHHo6t6mJunPrFR9JKXFCc0obwz5N-pxFLOc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", - implicitAssertion: "{\"test-vector\":\"3-E-7\"}", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}"), + implicitAssertion: []byte("{\"test-vector\":\"3-E-7\"}"), }, { name: "3-E-8", @@ -115,9 +115,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmZHSSKYR6AnPYJV6gpHtx6dLakIG_AOPhu8vKexNyrv5_1qoom6_NaPGecoiz6fR8.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", - implicitAssertion: "{\"test-vector\":\"3-E-8\"}", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}"), + implicitAssertion: []byte("{\"test-vector\":\"3-E-8\"}"), }, { name: "3-E-9", @@ -125,9 +125,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlk1nli0_wijTH_vCuRwckEDc82QWK8-lG2fT9wQF271sgbVRVPjm0LwMQZkvvamqU.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "arbitrary-string-that-isn't-json", - implicitAssertion: "{\"test-vector\":\"3-E-9\"}", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("arbitrary-string-that-isn't-json"), + implicitAssertion: []byte("{\"test-vector\":\"3-E-9\"}"), }, } @@ -154,12 +154,12 @@ func Test_Paseto_LocalVector(t *testing.T) { assert.Equal(t, testCase.token, string(token)) // Decrypt - message, err := Decrypt(key, []byte(testCase.token), testCase.footer, testCase.implicitAssertion) + message, err := Decrypt(key, testCase.token, testCase.footer, testCase.implicitAssertion) if (err != nil) != testCase.expectFail { t.Errorf("error during the decrypt call, error = %v, wantErr %v", err, testCase.expectFail) return } - assert.Equal(t, testCase.payload, string(message)) + assert.Equal(t, testCase.payload, message) }) } } @@ -172,8 +172,8 @@ func Test_Paseto_Local_EncryptDecrypt(t *testing.T) { assert.NoError(t, err) m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"4-S-3\"}") token1, err := Encrypt(rand.Reader, key, m, f, i) assert.NoError(t, err) @@ -192,7 +192,7 @@ func Test_Paseto_Local_EncryptDecrypt(t *testing.T) { // ----------------------------------------------------------------------------- -func benchmarkEncrypt(key *LocalKey, m []byte, f, i string, b *testing.B) { +func benchmarkEncrypt(key *LocalKey, m, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { _, err := Encrypt(rand.Reader, key, m, f, i) if err != nil { @@ -208,8 +208,8 @@ func Benchmark_Paseto_Encrypt(b *testing.B) { key := LocalKey(keyRaw) m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"3-E-3\"}" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"3-E-3\"}") b.ReportAllocs() b.ResetTimer() @@ -217,9 +217,9 @@ func Benchmark_Paseto_Encrypt(b *testing.B) { benchmarkEncrypt(&key, m, f, i, b) } -func benchmarkDecrypt(key *LocalKey, m []byte, f, i string, b *testing.B) { +func benchmarkDecrypt(key *LocalKey, t string, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { - _, err := Decrypt(key, m, f, i) + _, err := Decrypt(key, t, f, i) if err != nil { b.Fatal(err) } @@ -232,12 +232,12 @@ func Benchmark_Paseto_Decrypt(b *testing.B) { assert.NoError(b, err) key := LocalKey(keyRaw) - m := []byte("v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmZHSSKYR6AnPYJV6gpHtx6dLakIG_AOPhu8vKexNyrv5_1qoom6_NaPGecoiz6fR8.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9") - f := "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}" - i := "{\"test-vector\":\"3-E-8\"}" + t := "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmZHSSKYR6AnPYJV6gpHtx6dLakIG_AOPhu8vKexNyrv5_1qoom6_NaPGecoiz6fR8.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9" + f := []byte("{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}") + i := []byte("{\"test-vector\":\"3-E-8\"}") b.ReportAllocs() b.ResetTimer() - benchmarkDecrypt(&key, m, f, i, b) + benchmarkDecrypt(&key, t, f, i, b) } diff --git a/v3/public.go b/v3/public.go index bf6b19e..e7bb087 100644 --- a/v3/public.go +++ b/v3/public.go @@ -22,6 +22,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/sha512" + "crypto/subtle" "encoding/base64" "errors" "fmt" @@ -34,20 +35,17 @@ import ( // Sign a message (m) with the private key (sk). // PASETO v3 public signature primitive. // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version3.md#sign -func Sign(m []byte, sk *ecdsa.PrivateKey, f, i string) ([]byte, error) { +func Sign(m []byte, sk *ecdsa.PrivateKey, f, i []byte) (string, error) { // Check arguments if sk == nil { - return nil, errors.New("paseto: unable to sign with a nil private key") + return "", errors.New("paseto: unable to sign with a nil private key") } // Compress public key point pk := elliptic.MarshalCompressed(elliptic.P384(), sk.X, sk.Y) // Compute protected content - m2, err := common.PreAuthenticationEncoding(pk, []byte(PublicPrefix), m, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("paseto: unable to prepare protected content: %w", err) - } + m2 := common.PreAuthenticationEncoding(pk, []byte(PublicPrefix), m, f, i) // Compute SHA-384 digest digest := sha512.Sum384(m2) @@ -55,74 +53,78 @@ func Sign(m []byte, sk *ecdsa.PrivateKey, f, i string) ([]byte, error) { // Sign using a determistic ECDSA scheme r, s := rfc6979.SignECDSA(sk, digest[:], sha512.New384) - // Assemble signature - sig := append(r.Bytes(), s.Bytes()...) - // Prepare content - body := append([]byte{}, m...) - body = append(body, sig...) + body := make([]byte, 0, len(m)+r.BitLen()/8+s.BitLen()/8) + body = append(body, m...) + body = append(body, r.Bytes()...) + body = append(body, s.Bytes()...) // Encode body as RawURLBase64 - encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) - base64.RawURLEncoding.Encode(encodedBody, body) + tokenLen := base64.RawURLEncoding.EncodedLen(len(body)) + footerLen := base64.RawURLEncoding.EncodedLen(len(f)) + 1 + if len(f) > 0 { + tokenLen += base64.RawURLEncoding.EncodedLen(len(f)) + 1 + } + + final := make([]byte, 10+tokenLen) + copy(final, []byte(PublicPrefix)) + base64.RawURLEncoding.Encode(final[10:], body) // Assemble final token - final := append([]byte(PublicPrefix), encodedBody...) - if f != "" { + if len(f) > 0 { + final[10+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) - base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) - - // Assemble body and footer - final = append(final, append([]byte("."), encodedFooter...)...) + base64.RawURLEncoding.Encode(final[10+tokenLen-footerLen+1:], []byte(f)) } // No error - return final, nil + return string(final), nil } // PASETO v3 signature verification primitive. // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version3.md#verify -func Verify(sm []byte, pub *ecdsa.PublicKey, f, i string) ([]byte, error) { +func Verify(t string, pub *ecdsa.PublicKey, f, i []byte) ([]byte, error) { // Check arguments if pub == nil { return nil, errors.New("paseto: public key is nil") } + rawToken := []byte(t) + // Check token header - if !bytes.HasPrefix(sm, []byte(PublicPrefix)) { + if !bytes.HasPrefix(rawToken, []byte(PublicPrefix)) { return nil, errors.New("paseto: invalid token") } // Trim prefix - sm = sm[len(PublicPrefix):] + rawToken = rawToken[len(PublicPrefix):] // Check footer usage - if f != "" { + if len(f) > 0 { // Split the footer and the body - parts := bytes.SplitN(sm, []byte("."), 2) - if len(parts) != 2 { + footerIdx := bytes.Index(rawToken, []byte(".")) + if footerIdx == 0 { return nil, errors.New("paseto: invalid token, footer is missing but expected") } // Decode footer - footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) - if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { + footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken[footerIdx+1:]))) + if _, err := base64.RawURLEncoding.Decode(footer, rawToken[footerIdx+1:]); err != nil { return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) } // Compare footer - if !common.SecureCompare([]byte(f), footer) { + if subtle.ConstantTimeCompare(f, footer) == 0 { return nil, errors.New("paseto: invalid token, footer mismatch") } // Continue without footer - sm = parts[0] + rawToken = rawToken[:footerIdx] } // Decode token - raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(sm))) - if _, err := base64.RawURLEncoding.Decode(raw, sm); err != nil { + raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken))) + if _, err := base64.RawURLEncoding.Decode(raw, rawToken); err != nil { return nil, fmt.Errorf("paseto: invalid token body: %w", err) } @@ -134,10 +136,7 @@ func Verify(sm []byte, pub *ecdsa.PublicKey, f, i string) ([]byte, error) { pk := elliptic.MarshalCompressed(elliptic.P384(), pub.X, pub.Y) // Compute protected content - m2, err := common.PreAuthenticationEncoding(pk, []byte(PublicPrefix), m, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to prepare protected content: %w", err) - } + m2 := common.PreAuthenticationEncoding(pk, []byte(PublicPrefix), m, f, i) // Compute SHA-384 digest digest := sha512.Sum384(m2) diff --git a/v3/public_test.go b/v3/public_test.go index 713423a..63ec54a 100644 --- a/v3/public_test.go +++ b/v3/public_test.go @@ -89,7 +89,7 @@ func Test_Paseto_PublicVector(t *testing.T) { sk.PublicKey.X, sk.PublicKey.Y = sk.PublicKey.Curve.ScalarBaseMult(sk.D.Bytes()) // Sign - token, err := Sign([]byte(testCase.payload), &sk, testCase.footer, testCase.implicitAssertion) + token, err := Sign([]byte(testCase.payload), &sk, []byte(testCase.footer), []byte(testCase.implicitAssertion)) if (err != nil) != testCase.expectFail { t.Errorf("error during the sign call, error = %v, wantErr %v", err, testCase.expectFail) return @@ -97,7 +97,7 @@ func Test_Paseto_PublicVector(t *testing.T) { assert.Equal(t, testCase.token, string(token)) // Verify - message, err := Verify([]byte(testCase.token), &sk.PublicKey, testCase.footer, testCase.implicitAssertion) + message, err := Verify(testCase.token, &sk.PublicKey, []byte(testCase.footer), []byte(testCase.implicitAssertion)) if (err != nil) != testCase.expectFail { t.Errorf("error during the verify call, error = %v, wantErr %v", err, testCase.expectFail) return @@ -109,7 +109,7 @@ func Test_Paseto_PublicVector(t *testing.T) { // ----------------------------------------------------------------------------- -func benchmarkSign(m []byte, sk *ecdsa.PrivateKey, f, i string, b *testing.B) { +func benchmarkSign(m []byte, sk *ecdsa.PrivateKey, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { _, err := Sign(m, sk, f, i) if err != nil { @@ -125,8 +125,8 @@ func Benchmark_Paseto_Sign(b *testing.B) { sk.PublicKey.X, sk.PublicKey.Y = sk.PublicKey.Curve.ScalarBaseMult(sk.D.Bytes()) m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"4-S-3\"}") b.ReportAllocs() b.ResetTimer() @@ -134,9 +134,9 @@ func Benchmark_Paseto_Sign(b *testing.B) { benchmarkSign(m, &sk, f, i, b) } -func benchmarkVerify(m []byte, pk *ecdsa.PublicKey, f, i string, b *testing.B) { +func benchmarkVerify(t string, pk *ecdsa.PublicKey, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { - _, err := Verify(m, pk, f, i) + _, err := Verify(t, pk, f, i) if err != nil { b.Fatal(err) } @@ -149,9 +149,9 @@ func Benchmark_Paseto_Verify(b *testing.B) { sk.PublicKey.Curve = elliptic.P384() sk.PublicKey.X, sk.PublicKey.Y = sk.PublicKey.Curve.ScalarBaseMult(sk.D.Bytes()) - token := []byte("v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ94SjWIbjmS7715GjLSnHnpJrC9Z-cnwK45dmvnVvCRQDCCKAXaKEopTajX0DKYx1Xqr6gcTdfqscLCAbiB4eOW9jlt-oNqdG8TjsYEi6aloBfTzF1DXff_45tFlnBukEX.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9") - f := "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}" - i := "{\"test-vector\":\"3-S-3\"}" + token := "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ94SjWIbjmS7715GjLSnHnpJrC9Z-cnwK45dmvnVvCRQDCCKAXaKEopTajX0DKYx1Xqr6gcTdfqscLCAbiB4eOW9jlt-oNqdG8TjsYEi6aloBfTzF1DXff_45tFlnBukEX.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9" + f := []byte("{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}") + i := []byte("{\"test-vector\":\"3-S-3\"}") b.ReportAllocs() b.ResetTimer() diff --git a/v4/helpers.go b/v4/helpers.go index b09cdee..093d4bf 100644 --- a/v4/helpers.go +++ b/v4/helpers.go @@ -43,10 +43,6 @@ func kdf(key *LocalKey, n []byte) (ek, n2, ak []byte, err error) { encKDF.Write(n) tmp := encKDF.Sum(nil) - // Split encryption key (Ek) and nonce (n2) - ek = tmp[:KeyLength] - n2 = tmp[KeyLength:] - // Derive authentication key authKDF, err := blake2b.New(authenticationKeyLength, key[:]) if err != nil { @@ -59,15 +55,12 @@ func kdf(key *LocalKey, n []byte) (ek, n2, ak []byte, err error) { ak = authKDF.Sum(nil) // No error - return ek, n2, ak, nil + return tmp[:KeyLength], tmp[KeyLength:], ak, nil } -func mac(ak []byte, h string, n, c []byte, f, i string) ([]byte, error) { +func mac(ak, h, n, c, f, i []byte) ([]byte, error) { // Compute pre-authentication message - preAuth, err := common.PreAuthenticationEncoding([]byte(h), n, c, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to compute pre-authentication content: %w", err) - } + preAuth := common.PreAuthenticationEncoding(h, n, c, f, i) // Compute MAC mac, err := blake2b.New(macLength, ak) diff --git a/v4/local.go b/v4/local.go index 4f296d5..7a1c8b4 100644 --- a/v4/local.go +++ b/v4/local.go @@ -19,14 +19,13 @@ package v4 import ( "bytes" + "crypto/subtle" "encoding/base64" "errors" "fmt" "io" "golang.org/x/crypto/chacha20" - - "zntr.io/paseto/internal/common" ) // GenerateLocalKey generates a key for local encryption. @@ -57,71 +56,76 @@ func LocalKeyFromSeed(seed []byte) (*LocalKey, error) { // PASETO v4 symmetric encryption primitive. // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#encrypt -func Encrypt(r io.Reader, key *LocalKey, m []byte, f, i string) ([]byte, error) { +func Encrypt(r io.Reader, key *LocalKey, m, f, i []byte) (string, error) { // Check arguments if key == nil { - return nil, errors.New("paseto: key is nil") + return "", errors.New("paseto: key is nil") } if len(key) != KeyLength { - return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) + return "", fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) } + rawPrefix := []byte(LocalPrefix) + + // Pre-allocate body + body := make([]byte, nonceLength+len(m), nonceLength+len(m)+macLength) + // Create random seed - var n [nonceLength]byte - if _, err := io.ReadFull(r, n[:]); err != nil { - return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err) + if _, err := io.ReadFull(r, body[:nonceLength]); err != nil { + return "", fmt.Errorf("paseto: unable to generate random seed: %w", err) } // Derive keys from seed and secret key - ek, n2, ak, err := kdf(key, n[:]) + ek, n2, ak, err := kdf(key, body[:nonceLength]) if err != nil { - return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err) + return "", fmt.Errorf("paseto: unable to derive keys from seed: %w", err) } // Prepare XChaCha20 stream cipher (nonce > 24bytes => XChacha) ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2) if err != nil { - return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err) + return "", fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err) } // Encrypt the payload - c := make([]byte, len(m)) - ciph.XORKeyStream(c, m) + ciph.XORKeyStream(body[nonceLength:], m) // Compute MAC - t, err := mac(ak, LocalPrefix, n[:], c, f, i) + t, err := mac(ak, rawPrefix, body[:nonceLength], body[nonceLength:], f, i) if err != nil { - return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) + return "", fmt.Errorf("paseto: unable to compute MAC: %w", err) } // Serialize final token // h || base64url(n || c || t) - body := append([]byte{}, n[:]...) - body = append(body, c...) body = append(body, t...) // Encode body as RawURLBase64 - encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) - base64.RawURLEncoding.Encode(encodedBody, body) + tokenLen := base64.RawURLEncoding.EncodedLen(len(body)) + footerLen := 0 + if len(f) > 0 { + footerLen = base64.RawURLEncoding.EncodedLen(len(f)) + 1 + tokenLen += footerLen + } + + final := make([]byte, len(LocalPrefix)+tokenLen) + copy(final, rawPrefix) + base64.RawURLEncoding.Encode(final[9:], body) // Assemble final token - final := append([]byte(LocalPrefix), encodedBody...) - if f != "" { + if len(f) > 0 { + final[9+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) - base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) - - // Assemble body and footer - final = append(final, append([]byte("."), encodedFooter...)...) + base64.RawURLEncoding.Encode(final[9+tokenLen-footerLen+1:], []byte(f)) } // No error - return final, nil + return string(final), nil } // PASETO v4 symmetric decryption primitive // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#decrypt -func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { +func Decrypt(key *LocalKey, input string, f, i []byte) ([]byte, error) { // Check arguments if key == nil { return nil, errors.New("paseto: key is nil") @@ -129,44 +133,46 @@ func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { if len(key) != KeyLength { return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) } - if input == nil { - return nil, errors.New("paseto: input is nil") + if input == "" { + return nil, errors.New("paseto: input is blank") } + rawToken := []byte(input) + // Check token header - if !bytes.HasPrefix(input, []byte(LocalPrefix)) { + if !bytes.HasPrefix(rawToken, []byte(LocalPrefix)) { return nil, errors.New("paseto: invalid token") } // Trim prefix - input = input[len(LocalPrefix):] + rawToken = rawToken[len(LocalPrefix):] // Check footer usage - if f != "" { + if len(f) > 0 { // Split the footer and the body - parts := bytes.SplitN(input, []byte("."), 2) - if len(parts) != 2 { + footerIdx := bytes.Index(rawToken, []byte(".")) + if footerIdx == 0 { return nil, errors.New("paseto: invalid token, footer is missing but expected") } // Decode footer - footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) - if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { + footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken[footerIdx+1:]))) + if _, err := base64.RawURLEncoding.Decode(footer, rawToken[footerIdx+1:]); err != nil { return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) } // Compare footer - if !common.SecureCompare([]byte(f), footer) { + if subtle.ConstantTimeCompare(f, footer) == 0 { return nil, errors.New("paseto: invalid token, footer mismatch") } // Continue without footer - input = parts[0] + rawToken = rawToken[:footerIdx] } // Decode token - raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(input))) - if _, err := base64.RawURLEncoding.Decode(raw, input); err != nil { + raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken))) + if _, err := base64.RawURLEncoding.Decode(raw, rawToken); err != nil { return nil, fmt.Errorf("paseto: invalid token body: %w", err) } @@ -182,13 +188,13 @@ func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { } // Compute MAC - t2, err := mac(ak, LocalPrefix, n, c, f, i) + t2, err := mac(ak, []byte(LocalPrefix), n, c, f, i) if err != nil { return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) } // Time-constant compare MAC - if !common.SecureCompare(t, t2) { + if subtle.ConstantTimeCompare(t, t2) == 0 { return nil, errors.New("paseto: invalid pre-authentication header") } @@ -199,9 +205,8 @@ func Decrypt(key *LocalKey, input []byte, f, i string) ([]byte, error) { } // Decrypt the payload - m := make([]byte, len(c)) - ciph.XORKeyStream(m, c) + ciph.XORKeyStream(c, c) // No error - return m, nil + return c, nil } diff --git a/v4/local_test.go b/v4/local_test.go index 872ab30..534caba 100644 --- a/v4/local_test.go +++ b/v4/local_test.go @@ -34,9 +34,9 @@ func Test_Paseto_LocalVector(t *testing.T) { key string nonce string token string - payload string - footer string - implicitAssertion string + payload []byte + footer []byte + implicitAssertion []byte }{ { name: "4-E-1", @@ -44,9 +44,9 @@ func Test_Paseto_LocalVector(t *testing.T) { key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", nonce: "0000000000000000000000000000000000000000000000000000000000000000", token: "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvSwscFlAl1pk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XJ5hOb_4v9RmDkneN0S92dx0OW4pgy7omxgf3S8c3LlQg", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { name: "4-E-2", @@ -54,9 +54,9 @@ func Test_Paseto_LocalVector(t *testing.T) { key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", nonce: "0000000000000000000000000000000000000000000000000000000000000000", token: "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvS2csCgglvpk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XIemu9chy3WVKvRBfg6t8wwYHK0ArLxxfZP73W_vfwt5A", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { name: "4-E-3", @@ -64,9 +64,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6-tyebyWG6Ov7kKvBdkrrAJ837lKP3iDag2hzUPHuMKA", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { name: "4-E-4", @@ -74,9 +74,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4gt6TiLm55vIH8c_lGxxZpE3AWlH4WTR0v45nsWoU3gQ", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte(""), + implicitAssertion: []byte(""), }, { name: "4-E-5", @@ -84,9 +84,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4x-RMNXtQNbz7FvFZ_G-lFpk5RG3EOrwDL6CgDqcerSQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"), + implicitAssertion: []byte(""), }, { name: "4-E-6", @@ -94,9 +94,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6pWSA5HX2wjb3P-xLQg5K5feUCX4P2fpVK3ZLWFbMSxQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"), + implicitAssertion: []byte(""), }, { name: "4-E-7", @@ -104,9 +104,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t40KCCWLA7GYL9KFHzKlwY9_RnIfRrMQpueydLEAZGGcA.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "{\"test-vector\":\"4-E-7\"}", + payload: []byte("{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"), + implicitAssertion: []byte("{\"test-vector\":\"4-E-7\"}"), }, { name: "4-E-8", @@ -114,9 +114,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t5uvqQbMGlLLNYBc7A6_x7oqnpUK5WLvj24eE4DVPDZjw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "{\"test-vector\":\"4-E-8\"}", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"), + implicitAssertion: []byte("{\"test-vector\":\"4-E-8\"}"), }, { name: "4-E-9", @@ -124,9 +124,9 @@ func Test_Paseto_LocalVector(t *testing.T) { nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6tybdlmnMwcDMw0YxA_gFSE_IUWl78aMtOepFYSWYfQA.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "arbitrary-string-that-isn't-json", - implicitAssertion: "{\"test-vector\":\"4-E-9\"}", + payload: []byte("{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), + footer: []byte("arbitrary-string-that-isn't-json"), + implicitAssertion: []byte("{\"test-vector\":\"4-E-9\"}"), }, } @@ -145,7 +145,7 @@ func Test_Paseto_LocalVector(t *testing.T) { assert.NoError(t, err) // Encrypt - token, err := Encrypt(bytes.NewReader(n), key, []byte(testCase.payload), testCase.footer, testCase.implicitAssertion) + token, err := Encrypt(bytes.NewReader(n), key, testCase.payload, testCase.footer, testCase.implicitAssertion) if (err != nil) != testCase.expectFail { t.Errorf("error during the encrypt call, error = %v, wantErr %v", err, testCase.expectFail) return @@ -153,12 +153,12 @@ func Test_Paseto_LocalVector(t *testing.T) { assert.Equal(t, testCase.token, string(token)) // Decrypt - message, err := Decrypt(key, []byte(testCase.token), testCase.footer, testCase.implicitAssertion) + message, err := Decrypt(key, testCase.token, testCase.footer, testCase.implicitAssertion) if (err != nil) != testCase.expectFail { t.Errorf("error during the decrypt call, error = %v, wantErr %v", err, testCase.expectFail) return } - assert.Equal(t, testCase.payload, string(message)) + assert.Equal(t, testCase.payload, message) }) } } @@ -171,8 +171,8 @@ func Test_Paseto_Local_EncryptDecrypt(t *testing.T) { assert.NoError(t, err) m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"4-S-3\"}") token1, err := Encrypt(rand.Reader, key, m, f, i) assert.NoError(t, err) @@ -191,7 +191,7 @@ func Test_Paseto_Local_EncryptDecrypt(t *testing.T) { // ----------------------------------------------------------------------------- -func benchmarkEncrypt(key *LocalKey, m []byte, f, i string, b *testing.B) { +func benchmarkEncrypt(key *LocalKey, m, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { _, err := Encrypt(rand.Reader, key, m, f, i) if err != nil { @@ -207,8 +207,8 @@ func Benchmark_Paseto_Encrypt(b *testing.B) { key := LocalKey(keyRaw) m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"4-S-3\"}") b.ReportAllocs() b.ResetTimer() @@ -216,9 +216,9 @@ func Benchmark_Paseto_Encrypt(b *testing.B) { benchmarkEncrypt(&key, m, f, i, b) } -func benchmarkDecrypt(key *LocalKey, m []byte, f, i string, b *testing.B) { +func benchmarkDecrypt(key *LocalKey, t string, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { - _, err := Decrypt(key, m, f, i) + _, err := Decrypt(key, t, f, i) if err != nil { b.Fatal(err) } @@ -231,12 +231,12 @@ func Benchmark_Paseto_Decrypt(b *testing.B) { assert.NoError(b, err) key := LocalKey(keyRaw) - m := []byte("v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t5uvqQbMGlLLNYBc7A6_x7oqnpUK5WLvj24eE4DVPDZjw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-E-8\"}" + t := "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t5uvqQbMGlLLNYBc7A6_x7oqnpUK5WLvj24eE4DVPDZjw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"4-E-8\"}") b.ReportAllocs() b.ResetTimer() - benchmarkDecrypt(&key, m, f, i, b) + benchmarkDecrypt(&key, t, f, i, b) } diff --git a/v4/public.go b/v4/public.go index 947a869..b7308e1 100644 --- a/v4/public.go +++ b/v4/public.go @@ -20,6 +20,7 @@ package v4 import ( "bytes" "crypto/ed25519" + "crypto/subtle" "encoding/base64" "errors" "fmt" @@ -30,76 +31,79 @@ import ( // Sign a message (m) with the private key (sk). // PASETO v4 public signature primitive. // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#sign -func Sign(m []byte, sk ed25519.PrivateKey, f, i string) ([]byte, error) { +func Sign(m []byte, sk ed25519.PrivateKey, f, i []byte) (string, error) { // Compute protected content - m2, err := common.PreAuthenticationEncoding([]byte(PublicPrefix), m, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to prepare protected content: %w", err) - } + m2 := common.PreAuthenticationEncoding([]byte(PublicPrefix), m, f, i) // Sign protected content sig := ed25519.Sign(sk, m2) // Prepare content - body := append([]byte{}, m...) + body := make([]byte, 0, len(m)+ed25519.SignatureSize) + body = append(body, m...) body = append(body, sig...) // Encode body as RawURLBase64 - encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) - base64.RawURLEncoding.Encode(encodedBody, body) + tokenLen := base64.RawURLEncoding.EncodedLen(len(body)) + footerLen := base64.RawURLEncoding.EncodedLen(len(f)) + 1 + if len(f) > 0 { + tokenLen += base64.RawURLEncoding.EncodedLen(len(f)) + 1 + } + + final := make([]byte, tokenLen+len(PublicPrefix)) + copy(final, []byte(PublicPrefix)) + base64.RawURLEncoding.Encode(final[10:], body) // Assemble final token - final := append([]byte(PublicPrefix), encodedBody...) - if f != "" { + if len(f) > 0 { + final[10+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) - base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) - - // Assemble body and footer - final = append(final, append([]byte("."), encodedFooter...)...) + base64.RawURLEncoding.Encode(final[10+tokenLen-footerLen+1:], []byte(f)) } // No error - return final, nil + return string(final), nil } // PASETO v4 signature verification primitive. // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#verify -func Verify(sm []byte, pk ed25519.PublicKey, f, i string) ([]byte, error) { +func Verify(t string, pk ed25519.PublicKey, f, i []byte) ([]byte, error) { + rawToken := []byte(t) + // Check token header - if !bytes.HasPrefix(sm, []byte(PublicPrefix)) { + if !bytes.HasPrefix(rawToken, []byte(PublicPrefix)) { return nil, errors.New("paseto: invalid token") } // Trim prefix - sm = sm[len(PublicPrefix):] + rawToken = rawToken[len(PublicPrefix):] // Check footer usage - if f != "" { + if len(f) > 0 { // Split the footer and the body - parts := bytes.SplitN(sm, []byte("."), 2) - if len(parts) != 2 { + footerIdx := bytes.Index(rawToken, []byte(".")) + if footerIdx == 0 { return nil, errors.New("paseto: invalid token, footer is missing but expected") } // Decode footer - footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) - if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { + footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken[footerIdx+1:]))) + if _, err := base64.RawURLEncoding.Decode(footer, rawToken[footerIdx+1:]); err != nil { return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) } // Compare footer - if !common.SecureCompare([]byte(f), footer) { + if subtle.ConstantTimeCompare(f, footer) == 0 { return nil, errors.New("paseto: invalid token, footer mismatch") } // Continue without footer - sm = parts[0] + rawToken = rawToken[:footerIdx] } // Decode token - raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(sm))) - if _, err := base64.RawURLEncoding.Decode(raw, sm); err != nil { + raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(rawToken))) + if _, err := base64.RawURLEncoding.Decode(raw, rawToken); err != nil { return nil, fmt.Errorf("paseto: invalid token body: %w", err) } @@ -108,10 +112,7 @@ func Verify(sm []byte, pk ed25519.PublicKey, f, i string) ([]byte, error) { s := raw[len(raw)-ed25519.SignatureSize:] // Compute protected content - m2, err := common.PreAuthenticationEncoding([]byte(PublicPrefix), m, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to prepare protected content: %w", err) - } + m2 := common.PreAuthenticationEncoding([]byte(PublicPrefix), m, f, i) // Check signature if !ed25519.Verify(pk, m2, s) { diff --git a/v4/public_test.go b/v4/public_test.go index c86eb28..7cfb94e 100644 --- a/v4/public_test.go +++ b/v4/public_test.go @@ -36,7 +36,7 @@ func Test_Paseto_PublicVector(t *testing.T) { secretKeyPem string publicKeyPem string token string - payload string + payload []byte footer string implicitAssertion string }{ @@ -49,7 +49,7 @@ func Test_Paseto_PublicVector(t *testing.T) { secretKeyPem: "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", token: "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9bg_XBBzds8lTZShVlwwKSgeKpLT3yukTw6JUz3W4h_ExsQV-P0V54zemZDcAxFaSeef1QlXEFtkqxT1ciiQEDA", - payload: "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + payload: []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), footer: "", implicitAssertion: "", }, @@ -62,7 +62,7 @@ func Test_Paseto_PublicVector(t *testing.T) { secretKeyPem: "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", token: "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + payload: []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", implicitAssertion: "", }, @@ -75,7 +75,7 @@ func Test_Paseto_PublicVector(t *testing.T) { secretKeyPem: "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", token: "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + payload: []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"), footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", implicitAssertion: "{\"test-vector\":\"4-S-3\"}", }, @@ -100,7 +100,7 @@ func Test_Paseto_PublicVector(t *testing.T) { assert.Equal(t, publicKey, []byte(pk)) // Sign - token, err := Sign([]byte(testCase.payload), sk, testCase.footer, testCase.implicitAssertion) + token, err := Sign(testCase.payload, sk, []byte(testCase.footer), []byte(testCase.implicitAssertion)) if (err != nil) != testCase.expectFail { t.Errorf("error during the sign call, error = %v, wantErr %v", err, testCase.expectFail) return @@ -108,19 +108,19 @@ func Test_Paseto_PublicVector(t *testing.T) { assert.Equal(t, testCase.token, string(token)) // Verify - message, err := Verify([]byte(testCase.token), pk, testCase.footer, testCase.implicitAssertion) + message, err := Verify(testCase.token, pk, []byte(testCase.footer), []byte(testCase.implicitAssertion)) if (err != nil) != testCase.expectFail { t.Errorf("error during the verify call, error = %v, wantErr %v", err, testCase.expectFail) return } - assert.Equal(t, testCase.payload, string(message)) + assert.Equal(t, testCase.payload, message) }) } } // ----------------------------------------------------------------------------- -func benchmarkSign(m []byte, sk ed25519.PrivateKey, f, i string, b *testing.B) { +func benchmarkSign(m []byte, sk ed25519.PrivateKey, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { _, err := Sign(m, sk, f, i) if err != nil { @@ -134,8 +134,8 @@ func Benchmark_Paseto_Sign(b *testing.B) { assert.NoError(b, err) m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"4-S-3\"}") b.ReportAllocs() b.ResetTimer() @@ -143,7 +143,7 @@ func Benchmark_Paseto_Sign(b *testing.B) { benchmarkSign(m, sk, f, i, b) } -func benchmarkVerify(m []byte, pk ed25519.PublicKey, f, i string, b *testing.B) { +func benchmarkVerify(m string, pk ed25519.PublicKey, f, i []byte, b *testing.B) { for n := 0; n < b.N; n++ { _, err := Verify(m, pk, f, i) if err != nil { @@ -156,9 +156,9 @@ func Benchmark_Paseto_Verify(b *testing.B) { pk, err := hex.DecodeString("1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2") assert.NoError(b, err) - token := []byte("v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" + token := "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9" + f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") + i := []byte("{\"test-vector\":\"4-S-3\"}") b.ReportAllocs() b.ResetTimer()