From c17554047d22c0b819ac538f356fde1d2adbc3a1 Mon Sep 17 00:00:00 2001 From: Thibault Normand Date: Mon, 16 Jan 2023 12:37:28 +0100 Subject: [PATCH] feat(api): returns and consumes token as string Signed-off-by: Thibault Normand --- README.md | 20 ++++++++-------- example_test.go | 4 ++-- v3/local.go | 49 ++++++++++++++++++++------------------- v3/local_test.go | 10 ++++---- v3/public.go | 35 +++++++++++++++------------- v3/public_test.go | 8 +++---- v4/local.go | 58 ++++++++++++++++++++++++++--------------------- v4/local_test.go | 10 ++++---- v4/public.go | 33 +++++++++++++++------------ v4/public_test.go | 6 ++--- 10 files changed, 124 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 213e2b3..cdfbbd1 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,12 @@ More examples - [here](example_test.go) goos: darwin goarch: arm64 pkg: zntr.io/paseto/v3 -Benchmark_Paseto_Encrypt-10 74648 14735 ns/op 8288 B/op 60 allocs/op -Benchmark_Paseto_Decrypt-10 84492 14211 ns/op 7762 B/op 58 allocs/op -Benchmark_Paseto_Sign-10 7495 157311 ns/op 9076 B/op 87 allocs/op -Benchmark_Paseto_Verify-10 1976 604464 ns/op 3433 B/op 51 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 5.335s +ok zntr.io/paseto/v3 5.373s ``` ### V4 @@ -71,12 +71,12 @@ ok zntr.io/paseto/v3 5.335s goos: darwin goarch: arm64 pkg: zntr.io/paseto/v4 -Benchmark_Paseto_Encrypt-10 461188 2567 ns/op 2272 B/op 13 allocs/op -Benchmark_Paseto_Decrypt-10 570516 2086 ns/op 1776 B/op 11 allocs/op -Benchmark_Paseto_Sign-10 48141 24877 ns/op 912 B/op 5 allocs/op -Benchmark_Paseto_Verify-10 22591 52607 ns/op 416 B/op 3 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 6.588s +ok zntr.io/paseto/v4 6.660s ``` ## License diff --git a/example_test.go b/example_test.go index 002d920..9a7401e 100644 --- a/example_test.go +++ b/example_test.go @@ -114,7 +114,7 @@ func ExamplePasetoV4LocalDecrypt() { } // Encrypted token. - input := []byte("v4.local.dGVzdHMtMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTZ-qF7cj1LApZxpU5R2qdaX9Ox9NaKxnci6ObPVawSbAlqcRdmSDrklvbUqNGk61-tuOKJ0vkFQ.eyJraWQiOiIxMjM0NTY3ODkwIn0") + input := "v4.local.dGVzdHMtMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTZ-qF7cj1LApZxpU5R2qdaX9Ox9NaKxnci6ObPVawSbAlqcRdmSDrklvbUqNGk61-tuOKJ0vkFQ.eyJraWQiOiIxMjM0NTY3ODkwIn0" // Expected footer value. footer := []byte(`{"kid":"1234567890"}`) @@ -169,7 +169,7 @@ func ExamplePasetoV4PublicVerify() { } // Prepare the message - input := []byte("v4.public.bXkgc3VwZXIgc2VjcmV0IG1lc3NhZ2UbOO-zu6XQbbhmDj0IUEjrmLS_TK1vM69D3pmdbUJmSa7A4c0qjEi9q-DQiMD6UUtbGEMXA1z9zdRskpGfStQH.eyJraWQiOiIxMjM0NTY3ODkwIn0") + input := "v4.public.bXkgc3VwZXIgc2VjcmV0IG1lc3NhZ2UbOO-zu6XQbbhmDj0IUEjrmLS_TK1vM69D3pmdbUJmSa7A4c0qjEi9q-DQiMD6UUtbGEMXA1z9zdRskpGfStQH.eyJraWQiOiIxMjM0NTY3ODkwIn0" footer := []byte(`{"kid":"1234567890"}`) assertions := []byte(`{"user_id":"1234567890"}`) diff --git a/v3/local.go b/v3/local.go index b00d06e..e4d5fca 100644 --- a/v3/local.go +++ b/v3/local.go @@ -56,13 +56,13 @@ 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, f, i []byte) ([]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 @@ -70,19 +70,19 @@ func Encrypt(r io.Reader, key *LocalKey, m, f, i []byte) ([]byte, error) { // Create random seed if _, err := io.ReadFull(r, body[:nonceLength]); err != nil { - return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err) + return "", fmt.Errorf("paseto: unable to generate random seed: %w", err) } // Derive keys from seed and secret key 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) @@ -92,7 +92,7 @@ func Encrypt(r io.Reader, key *LocalKey, m, f, i []byte) ([]byte, error) { // Compute MAC 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 @@ -106,23 +106,24 @@ func Encrypt(r io.Reader, key *LocalKey, m, f, i []byte) ([]byte, error) { tokenLen += base64.RawURLEncoding.EncodedLen(len(f)) + 1 } - final := make([]byte, tokenLen) - base64.RawURLEncoding.Encode(final, body) + final := make([]byte, 9+tokenLen) + copy(final, []byte(LocalPrefix)) + base64.RawURLEncoding.Encode(final[9:], body) // Assemble final token if len(f) > 0 { - final[tokenLen-footerLen] = '.' + final[9+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - base64.RawURLEncoding.Encode(final[tokenLen-footerLen+1:], []byte(f)) + base64.RawURLEncoding.Encode(final[9+tokenLen-footerLen+1:], []byte(f)) } // No error - return append([]byte(LocalPrefix), 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, f, i []byte) ([]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,29 +131,31 @@ func Decrypt(key *LocalKey, input, f, i []byte) ([]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 len(f) > 0 { // Split the footer and the body - footerIdx := bytes.Index(input, []byte(".")) + 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(input[footerIdx+1:]))) - if _, err := base64.RawURLEncoding.Decode(footer, input[footerIdx+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) } @@ -162,12 +165,12 @@ func Decrypt(key *LocalKey, input, f, i []byte) ([]byte, error) { } // Continue without footer - input = input[:footerIdx] + 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) } diff --git a/v3/local_test.go b/v3/local_test.go index a65bb12..b48d417 100644 --- a/v3/local_test.go +++ b/v3/local_test.go @@ -154,7 +154,7 @@ 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 @@ -217,9 +217,9 @@ func Benchmark_Paseto_Encrypt(b *testing.B) { benchmarkEncrypt(&key, m, f, i, b) } -func benchmarkDecrypt(key *LocalKey, m, f, i []byte, 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") + 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 5600447..e7bb087 100644 --- a/v3/public.go +++ b/v3/public.go @@ -35,10 +35,10 @@ 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 []byte) ([]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 @@ -66,47 +66,50 @@ func Sign(m []byte, sk *ecdsa.PrivateKey, f, i []byte) ([]byte, error) { tokenLen += base64.RawURLEncoding.EncodedLen(len(f)) + 1 } - final := make([]byte, tokenLen) - base64.RawURLEncoding.Encode(final, body) + final := make([]byte, 10+tokenLen) + copy(final, []byte(PublicPrefix)) + base64.RawURLEncoding.Encode(final[10:], body) // Assemble final token if len(f) > 0 { - final[tokenLen-footerLen] = '.' + final[10+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - base64.RawURLEncoding.Encode(final[tokenLen-footerLen+1:], []byte(f)) + base64.RawURLEncoding.Encode(final[10+tokenLen-footerLen+1:], []byte(f)) } // No error - return append([]byte(PublicPrefix), 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 []byte) ([]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 len(f) > 0 { // Split the footer and the body - footerIdx := bytes.Index(sm, []byte(".")) + 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(sm[footerIdx+1:]))) - if _, err := base64.RawURLEncoding.Decode(footer, sm[footerIdx+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) } @@ -116,12 +119,12 @@ func Verify(sm []byte, pub *ecdsa.PublicKey, f, i []byte) ([]byte, error) { } // Continue without footer - sm = sm[:footerIdx] + 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) } diff --git a/v3/public_test.go b/v3/public_test.go index 9ddaaef..63ec54a 100644 --- a/v3/public_test.go +++ b/v3/public_test.go @@ -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, []byte(testCase.footer), []byte(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 @@ -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 []byte, 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,7 +149,7 @@ 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") + token := "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ94SjWIbjmS7715GjLSnHnpJrC9Z-cnwK45dmvnVvCRQDCCKAXaKEopTajX0DKYx1Xqr6gcTdfqscLCAbiB4eOW9jlt-oNqdG8TjsYEi6aloBfTzF1DXff_45tFlnBukEX.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9" f := []byte("{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}") i := []byte("{\"test-vector\":\"3-S-3\"}") diff --git a/v4/local.go b/v4/local.go index da8c139..7a1c8b4 100644 --- a/v4/local.go +++ b/v4/local.go @@ -56,42 +56,44 @@ 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, f, i []byte) ([]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 if _, err := io.ReadFull(r, body[:nonceLength]); err != nil { - return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err) + return "", fmt.Errorf("paseto: unable to generate random seed: %w", err) } // Derive keys from seed and secret key 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 ciph.XORKeyStream(body[nonceLength:], m) // Compute MAC - t, err := mac(ak, []byte(LocalPrefix), body[:nonceLength], body[nonceLength:], 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 @@ -100,28 +102,30 @@ func Encrypt(r io.Reader, key *LocalKey, m, f, i []byte) ([]byte, error) { // Encode body as RawURLBase64 tokenLen := base64.RawURLEncoding.EncodedLen(len(body)) - footerLen := base64.RawURLEncoding.EncodedLen(len(f)) + 1 + footerLen := 0 if len(f) > 0 { - tokenLen += base64.RawURLEncoding.EncodedLen(len(f)) + 1 + footerLen = base64.RawURLEncoding.EncodedLen(len(f)) + 1 + tokenLen += footerLen } - final := make([]byte, tokenLen) - base64.RawURLEncoding.Encode(final, body) + final := make([]byte, len(LocalPrefix)+tokenLen) + copy(final, rawPrefix) + base64.RawURLEncoding.Encode(final[9:], body) // Assemble final token if len(f) > 0 { - final[tokenLen-footerLen] = '.' + final[9+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - base64.RawURLEncoding.Encode(final[tokenLen-footerLen+1:], []byte(f)) + base64.RawURLEncoding.Encode(final[9+tokenLen-footerLen+1:], []byte(f)) } // No error - return append([]byte(LocalPrefix), 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, f, i []byte) ([]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,29 +133,31 @@ func Decrypt(key *LocalKey, input, f, i []byte) ([]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 len(f) > 0 { // Split the footer and the body - footerIdx := bytes.Index(input, []byte(".")) + 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(input[footerIdx+1:]))) - if _, err := base64.RawURLEncoding.Decode(footer, input[footerIdx+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) } @@ -161,12 +167,12 @@ func Decrypt(key *LocalKey, input, f, i []byte) ([]byte, error) { } // Continue without footer - input = input[:footerIdx] + 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) } diff --git a/v4/local_test.go b/v4/local_test.go index 67e7d8f..534caba 100644 --- a/v4/local_test.go +++ b/v4/local_test.go @@ -153,7 +153,7 @@ 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 @@ -216,9 +216,9 @@ func Benchmark_Paseto_Encrypt(b *testing.B) { benchmarkEncrypt(&key, m, f, i, b) } -func benchmarkDecrypt(key *LocalKey, m, f, i []byte, 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") + 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 048aed0..b7308e1 100644 --- a/v4/public.go +++ b/v4/public.go @@ -31,7 +31,7 @@ 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 []byte) ([]byte, error) { +func Sign(m []byte, sk ed25519.PrivateKey, f, i []byte) (string, error) { // Compute protected content m2 := common.PreAuthenticationEncoding([]byte(PublicPrefix), m, f, i) @@ -50,42 +50,45 @@ func Sign(m []byte, sk ed25519.PrivateKey, f, i []byte) ([]byte, error) { tokenLen += base64.RawURLEncoding.EncodedLen(len(f)) + 1 } - final := make([]byte, tokenLen) - base64.RawURLEncoding.Encode(final, body) + final := make([]byte, tokenLen+len(PublicPrefix)) + copy(final, []byte(PublicPrefix)) + base64.RawURLEncoding.Encode(final[10:], body) // Assemble final token if len(f) > 0 { - final[tokenLen-footerLen] = '.' + final[10+tokenLen-footerLen] = '.' // Encode footer as RawURLBase64 - base64.RawURLEncoding.Encode(final[tokenLen-footerLen+1:], []byte(f)) + base64.RawURLEncoding.Encode(final[10+tokenLen-footerLen+1:], []byte(f)) } // No error - return append([]byte(PublicPrefix), 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 []byte) ([]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 len(f) > 0 { // Split the footer and the body - footerIdx := bytes.Index(sm, []byte(".")) + 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(sm[footerIdx+1:]))) - if _, err := base64.RawURLEncoding.Decode(footer, sm[footerIdx+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) } @@ -95,12 +98,12 @@ func Verify(sm []byte, pk ed25519.PublicKey, f, i []byte) ([]byte, error) { } // Continue without footer - sm = sm[:footerIdx] + 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) } diff --git a/v4/public_test.go b/v4/public_test.go index 42a172b..7cfb94e 100644 --- a/v4/public_test.go +++ b/v4/public_test.go @@ -108,7 +108,7 @@ func Test_Paseto_PublicVector(t *testing.T) { assert.Equal(t, testCase.token, string(token)) // Verify - message, err := Verify([]byte(testCase.token), pk, []byte(testCase.footer), []byte(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 @@ -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 []byte, 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,7 +156,7 @@ func Benchmark_Paseto_Verify(b *testing.B) { pk, err := hex.DecodeString("1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2") assert.NoError(b, err) - token := []byte("v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9") + token := "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9" f := []byte("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}") i := []byte("{\"test-vector\":\"4-S-3\"}")