From 98ffb5a88f36b88ac526c7e89a97cc387a19c19b Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Fri, 12 Apr 2024 22:45:02 +0200 Subject: [PATCH] Implement v2.local (#6) --- go.mod | 2 + go.sum | 2 + testdata/v2.json | 167 +++++++++++++++++++++++++++++++++++++++++++++++ v2loc.go | 101 ++++++++++++++++++++++++++++ v2loc_test.go | 48 ++++++++++++++ 5 files changed, 320 insertions(+) create mode 100644 testdata/v2.json create mode 100644 v2loc.go create mode 100644 v2loc_test.go diff --git a/go.mod b/go.mod index 16f56c6..c4cbf88 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/cristalhq/paseto go 1.21 require golang.org/x/crypto v0.22.0 + +require golang.org/x/sys v0.19.0 // indirect diff --git a/go.sum b/go.sum index ce62e45..f3d6e98 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/testdata/v2.json b/testdata/v2.json new file mode 100644 index 0000000..3961178 --- /dev/null +++ b/testdata/v2.json @@ -0,0 +1,167 @@ +{ + "name": "PASETO v2 Test Vectors", + "tests": [ + { + "name": "2-E-1", + "expect-fail": false, + "nonce": "000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4PnW8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVODyfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-2", + "expect-fail": false, + "nonce": "000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-3", + "expect-fail": false, + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-4", + "expect-fail": false, + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DPbIxtjGvNRAwsLK7LcV8oQ", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-5", + "expect-fail": false, + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "2-E-6", + "expect-fail": false, + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "2-E-7", + "expect-fail": false, + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "2-E-8", + "expect-fail": false, + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "2-E-9", + "expect-fail": false, + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DoOJbyKBGPZG50XDZ6mbPtw.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "2-S-1", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGntTu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_DjJK2ZXC2SUYuOFM-Q_5Cw", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-S-2", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "2-S-3", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "2-F-1", + "expect-fail": true, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v2.local.pN9Y9kTFKnCskKr7B13IoceBabSTMS0LkUg3SeAqONg6EJsq9h-CLWdWaA_rMZX4MhGsOQn5I0EsIgYeOA2NPJZU0uulsahH-k871PBq.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": null, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"2-F-1\"}" + }, + { + "name": "2-F-2", + "expect-fail": true, + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.public.eyJpbnZhbGlkIjoidGhpcyBzaG91bGQgbmV2ZXIgZGVjb2RlIn1kgrdAMxcO3wFKXJrLa1cq-DB6V_b25KQ1hV_jpOS-uYBmsg8EMS4j6kl2g83iRsh73knLGr7Ik1AEOvUgyw0P.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": null, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "{\"test-vector\":\"2-F-2\"}" + }, + { + "name": "2-F-3", + "expect-fail": true, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.vXWMCh8nxf_RMqrLREJVOWyu01yRzb-miB6mkG1zQ8LS4_W5nQdTOpexZq482ReJ0sv5uFfAWRGpJaONiMqFaAAo-dsbWG2vo63xUmwFGxHNhu9plfFav2SaGDERFGn7IQ20gNQl87eOLaxf2GDsWdfu5hrFaQ.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": null, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"2-F-3\"}" + } + ] +} \ No newline at end of file diff --git a/v2loc.go b/v2loc.go new file mode 100644 index 0000000..5583959 --- /dev/null +++ b/v2loc.go @@ -0,0 +1,101 @@ +package paseto + +import ( + "crypto/rand" + "fmt" + "io" + + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + v2LocHeader = "v2.local." + v2NonceSize = chacha20poly1305.NonceSizeX +) + +func V2Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error) { + if randBytes == nil { + randBytes = make([]byte, v2NonceSize) + if _, err := io.ReadFull(rand.Reader, randBytes); err != nil { + return "", fmt.Errorf("read from crypto/rand.Reader: %w", err) + } + } + + payloadBytes, err := toBytes(payload) + if err != nil { + return "", fmt.Errorf("encode payload: %w", err) + } + + footerBytes, err := toBytes(footer) + if err != nil { + return "", fmt.Errorf("encode footer: %w", err) + } + + hash, err := blake2b.New(v2NonceSize, randBytes) + if err != nil { + return "", fmt.Errorf("create blake2b hash: %w", err) + } + if _, err := hash.Write(payloadBytes); err != nil { + return "", fmt.Errorf("hash payload: %w", err) + } + nonce := hash.Sum(nil) + + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return "", fmt.Errorf("create chacha20poly1305 cipher: %w", err) + } + + preAuth := pae([]byte(v2LocHeader), nonce, footerBytes) + + encryptedPayload := aead.Seal( + payloadBytes[:0], + nonce, + payloadBytes, + preAuth, + ) + body := append(nonce, encryptedPayload...) + + return buildToken(v2LocHeader, body, footerBytes), nil +} + +func V2Decrypt(token string, key []byte, payload, footer any) error { + body, footerBytes, err := splitToken(token, v2LocHeader) + if err != nil { + return fmt.Errorf("decode token: %w", err) + } + if len(body) < v2NonceSize { + return ErrIncorrectTokenFormat + } + + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return fmt.Errorf("create chacha20poly1305 cipher: %w", err) + } + + nonce, encryptedPayload := body[:v2NonceSize], body[v2NonceSize:] + preAuth := pae([]byte(v2LocHeader), nonce, footerBytes) + + decryptedPayload, err := aead.Open( + encryptedPayload[:0], + nonce, + encryptedPayload, + preAuth, + ) + if err != nil { + return ErrInvalidTokenAuth + } + + if payload != nil { + if err := fromBytes(decryptedPayload, payload); err != nil { + return fmt.Errorf("decode payload: %w", err) + } + } + + if footer != nil { + if err := fromBytes(footerBytes, footer); err != nil { + return fmt.Errorf("decode footer: %w", err) + } + } + return nil +} diff --git a/v2loc_test.go b/v2loc_test.go new file mode 100644 index 0000000..2385e8d --- /dev/null +++ b/v2loc_test.go @@ -0,0 +1,48 @@ +package paseto + +import ( + "encoding/hex" + "strings" + "testing" +) + +func TestV2Loc_Encrypt(t *testing.T) { + testCases := loadGoldenFile("testdata/v2.json") + + for _, tc := range testCases.Tests { + if tc.Key == "" || !strings.HasPrefix(tc.Token, v2LocHeader) { + continue + } + + t.Run(tc.Name, func(t *testing.T) { + key := mustHex(tc.Key) + payload := mustJSON(tc.Payload) + footer := mustJSON(tc.Footer) + nonce := mustHex(tc.Nonce) + + token, err := V2Encrypt(key, payload, footer, nonce) + if err != nil { + t.Fatal(err) + } + mustEqual(t, token, tc.Token) + }) + } +} + +func TestV2Loc_Decrypt(t *testing.T) { + testCases := loadGoldenFile("testdata/v2.json") + + for _, tc := range testCases.Tests { + if tc.Key == "" || !strings.HasPrefix(tc.Token, v2LocHeader) { + continue + } + + t.Run(tc.Name, func(t *testing.T) { + key := must(hex.DecodeString(tc.Key)) + var payload, footer any + + err := V2Decrypt(tc.Token, key, payload, footer) + mustOk(t, err) + }) + } +}