From 7ce1c04781bdbb052534aa740ab9722d5809087c Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Fri, 12 Apr 2024 21:28:19 +0200 Subject: [PATCH] Implement v1.local (#5) --- common.go | 117 ++++++++++++++++++++++++++++++++++++++ errors.go | 10 ++++ go.mod | 4 +- go.sum | 2 + paseto_test.go | 86 ++++++++++++++++++++++++++++ testdata/v1.json | 145 +++++++++++++++++++++++++++++++++++++++++++++++ v1loc.go | 139 +++++++++++++++++++++++++++++++++++++++++++++ v1loc_test.go | 48 ++++++++++++++++ 8 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 common.go create mode 100644 errors.go create mode 100644 go.sum create mode 100644 paseto_test.go create mode 100644 testdata/v1.json create mode 100644 v1loc.go create mode 100644 v1loc_test.go diff --git a/common.go b/common.go new file mode 100644 index 0000000..85ca3b6 --- /dev/null +++ b/common.go @@ -0,0 +1,117 @@ +package paseto + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "strings" +) + +func pae(pieces ...[]byte) []byte { + var buf bytes.Buffer + binary.Write(&buf, binary.LittleEndian, int64(len(pieces))) + + for _, p := range pieces { + binary.Write(&buf, binary.LittleEndian, int64(len(p))) + buf.Write(p) + } + return buf.Bytes() +} + +func toBytes(x any) ([]byte, error) { + switch v := x.(type) { + case nil: + return nil, nil + case string: + return []byte(v), nil + case []byte: + return v, nil + default: + return json.Marshal(v) + } +} + +func fromBytes(data []byte, x any) error { + switch f := x.(type) { + case *string: + *f = string(data) + case *[]byte: + *f = append(*f, data...) + default: + if err := json.Unmarshal(data, x); err != nil { + return fmt.Errorf("%v: %w", err, ErrDataUnmarshal) + } + } + return nil +} + +func splitToken(token, header string) ([]byte, []byte, error) { + if !strings.HasPrefix(token, header) { + return nil, nil, ErrIncorrectTokenHeader + } + + parts := bytes.Split([]byte(token[len(header):]), []byte(".")) + + var rawPayload, rawFooter []byte + switch len(parts) { + case 1: + rawPayload = parts[0] + case 2: + rawPayload = parts[0] + rawFooter = parts[1] + default: + return nil, nil, ErrIncorrectTokenFormat + } + + payload := make([]byte, b64DecodedLen(len(rawPayload))) + if _, err := b64Decode(payload, rawPayload); err != nil { + return nil, nil, fmt.Errorf("decode payload: %w", err) + } + + var footer []byte + if rawFooter != nil { + footer = make([]byte, b64DecodedLen(len(rawFooter))) + if _, err := b64Decode(footer, rawFooter); err != nil { + return nil, nil, fmt.Errorf("decode footer: %w", err) + } + } + return payload, footer, nil +} + +func buildToken(header string, body, footer []byte) string { + size := len(header) + b64EncodedLen(len(body)) + if len(footer) > 0 { + size += 1 + b64EncodedLen(len(footer)) + } + + token := make([]byte, size) + offset := 0 + offset += copy(token[offset:], header) + + b64Encode(token[offset:], body) + offset += b64EncodedLen(len(body)) + + if len(footer) > 0 { + offset += copy(token[offset:], ".") + b64Encode(token[offset:], footer) + } + return string(token) +} + +func b64Decode(dst, src []byte) (n int, err error) { + return base64.RawURLEncoding.Decode(dst, src) +} + +func b64DecodedLen(n int) int { + return base64.RawURLEncoding.DecodedLen(n) +} + +func b64Encode(dst, src []byte) { + base64.RawURLEncoding.Encode(dst, src) +} + +func b64EncodedLen(n int) int { + return base64.RawURLEncoding.EncodedLen(n) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..c4bb4b2 --- /dev/null +++ b/errors.go @@ -0,0 +1,10 @@ +package paseto + +import "errors" + +var ( + ErrDataUnmarshal = errors.New("can't unmarshal token data to the given type of value") + ErrInvalidTokenAuth = errors.New("invalid token authentication") + ErrIncorrectTokenFormat = errors.New("incorrect token format") + ErrIncorrectTokenHeader = errors.New("incorrect token header") +) diff --git a/go.mod b/go.mod index 80e1db2..16f56c6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module paseto +module github.com/cristalhq/paseto go 1.21 + +require golang.org/x/crypto v0.22.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ce62e45 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= diff --git a/paseto_test.go b/paseto_test.go new file mode 100644 index 0000000..6f5a58e --- /dev/null +++ b/paseto_test.go @@ -0,0 +1,86 @@ +package paseto + +import ( + "encoding/hex" + "encoding/json" + "os" + "reflect" + "testing" +) + +type GoldenCases struct { + Tests []GoldenCase `json:"tests"` +} + +type GoldenCase struct { + Name string `json:"name"` + ExpectFail bool `json:"expect-fail"` + Nonce string `json:"nonce"` + Key string `json:"key"` + PublicKey string `json:"public-key"` + SecretKey string `json:"secret-key"` + SecretKeySeed string `json:"secret-key-seed"` + SecretKeyPem string `json:"secret-key-pem"` + PublicKeyPem string `json:"public-key-pem"` + Token string `json:"token"` + Payload string `json:"payload"` + Footer string `json:"footer"` + ImplicitAssertion string `json:"implicit-assertion"` +} + +func loadGoldenFile(filename string) GoldenCases { + f, err := os.Open(filename) + if err != nil { + panic(err) + } + defer f.Close() + + var tc GoldenCases + if err := json.NewDecoder(f).Decode(&tc); err != nil { + panic(err) + } + return tc +} + +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +func mustHex(raw string) []byte { + return must(hex.DecodeString(raw)) +} + +func mustJSON(raw string) any { + if len(raw) == 0 || string(raw) == "" { + return nil + } + var dst any + if err := json.Unmarshal([]byte(raw), &dst); err != nil { + return string(raw) + } + return dst +} + +func mustOk(tb testing.TB, err error) { + tb.Helper() + if err != nil { + tb.Fatal(err) + } +} + +func mustFail(tb testing.TB, err error) { + tb.Helper() + if err == nil { + tb.Fatal() + } +} + +func mustEqual[T any](tb testing.TB, have, want T) { + tb.Helper() + if !reflect.DeepEqual(have, want) { + tb.Fatalf("\nhave: %+v\nwant: %+v\n", have, want) + } +} diff --git a/testdata/v1.json b/testdata/v1.json new file mode 100644 index 0000000..cff3b12 --- /dev/null +++ b/testdata/v1.json @@ -0,0 +1,145 @@ +{ + "name": "PASETO v1 Test Vectors", + "tests": [ + { + "name": "1-E-1", + "expect-fail": false, + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-2", + "expect-fail": false, + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.w_NOpjgte4bX-2i1JAiTQzHoGUVOgc2yqKqsnYGmaPaCu_KWUkRGlCRnOvZZxeH4HTykY7AE_jkzSXAYBkQ1QnwvKS16uTXNfnmp8IRknY76I2m3S5qsM8klxWQQKFDuQHl8xXV0MwAoeFh9X6vbwIqrLlof3s4PMjRDwKsxYzkMr1RvfDI8emoPoW83q4Q60_xpHaw", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-3", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3syBYyjKIOeWnsFQB6Yef-1ov9rvqt7TmwONUHeJUYk4IK_JEdUeo_uFRqAIgHsiGCg", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-4", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvejdt2Srz_5Q0QG4oiz1gB_wmv4U5pifedaZbHXUTWXchFEi0etJ4u6tqgxZSklcec", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-5", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZEWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "1-E-6", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "1-E-7", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZEWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "1-E-8", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "1-E-9", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvdgNpe3vI21jV2YL7WVG5p63_JxxzLckBu9azQ0GlDMdPxNAxoyvmU1wbpSbRB9Iw4.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": "1-S-1", + "expect-fail": false, + "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", + "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", + "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9cIZKahKeGM5kiAS_4D70Qbz9FIThZpxetJ6n6E6kXP_119SvQcnfCSfY_gG3D0Q2v7FEtm2Cmj04lE6YdgiZ0RwA41WuOjXq7zSnmmHK9xOSH6_2yVgt207h1_LphJzVztmZzq05xxhZsV3nFPm2cCu8oPceWy-DBKjALuMZt_Xj6hWFFie96SfQ6i85lOsTX8Kc6SQaG-3CgThrJJ6W9DC-YfQ3lZ4TJUoY3QNYdtEgAvp1QuWWK6xmIb8BwvkBPej5t88QUb7NcvZ15VyNw3qemQGn2ITSdpdDgwMtpflZOeYdtuxQr1DSGO2aQyZl7s0WYn1IjdQFx6VjSQ4yfw", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-S-2", + "expect-fail": false, + "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", + "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", + "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9sBTIb0J_4misAuYc4-6P5iR1rQighzktpXhJ8gtrrp2MqSSDkbb8q5WZh3FhUYuW_rg2X8aflDlTWKAqJkM3otjYwtmfwfOhRyykxRL2AfmIika_A-_MaLp9F0iw4S1JetQQDV8GUHjosd87TZ20lT2JQLhxKjBNJSwWue8ucGhTgJcpOhXcthqaz7a2yudGyd0layzeWziBhdQpoBR6ryTdtIQX54hP59k3XCIxuYbB9qJMpixiPAEKBcjHT74sA-uukug9VgKO7heWHwJL4Rl9ad21xyNwaxAnwAJ7C0fN5oGv8Rl0dF11b3tRmsmbDoIokIM0Dba29x_T3YzOyg.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", + "implicit-assertion": "" + }, + { + "name": "1-S-3", + "expect-fail": false, + "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", + "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", + "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9sBTIb0J_4misAuYc4-6P5iR1rQighzktpXhJ8gtrrp2MqSSDkbb8q5WZh3FhUYuW_rg2X8aflDlTWKAqJkM3otjYwtmfwfOhRyykxRL2AfmIika_A-_MaLp9F0iw4S1JetQQDV8GUHjosd87TZ20lT2JQLhxKjBNJSwWue8ucGhTgJcpOhXcthqaz7a2yudGyd0layzeWziBhdQpoBR6ryTdtIQX54hP59k3XCIxuYbB9qJMpixiPAEKBcjHT74sA-uukug9VgKO7heWHwJL4Rl9ad21xyNwaxAnwAJ7C0fN5oGv8Rl0dF11b3tRmsmbDoIokIM0Dba29x_T3YzOyg.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "1-F-1", + "expect-fail": true, + "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", + "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", + "token": "v1.local.uLSnLZApN_-wP2fM5d-0DgQ4RoR8zO6Sp4p5VbYkSW_KuWt_1sbtDalN3HBtGR8PAtU5M_-bes-iSqppFtmykiznxvxvz30K8yXJ2aXGbS-upJKXjO5cjWXBGJLNBPzzdpUo85Iv-gW8bbZ9e4tfnNdsRcJZ-g.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": null, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"1-F-1\"}" + }, + { + "name": "1-F-2", + "expect-fail": true, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.gc3ZXdoGWUhRtJzu2YjbIRBvBwBU0PACGI-bB_2yNvzN_de8k-tH9_NS6gl1-EhJui7UZfcYymVqExSLAD9qoxoljAV9jstADgbcnBsS.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": null, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"1-F-3\"}" + } + ] +} \ No newline at end of file diff --git a/v1loc.go b/v1loc.go new file mode 100644 index 0000000..0e3551e --- /dev/null +++ b/v1loc.go @@ -0,0 +1,139 @@ +package paseto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +const ( + v1LocNonceSize = 32 + v1LocNonceHalf = v1LocNonceSize / 2 + v1LocMacSize = 48 // const for crypty.SHA384.Size() + v1LocHeader = "v1.local." +) + +func V1Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error) { + if randBytes == nil { + randBytes = make([]byte, v1LocNonceSize) + 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) + } + + macN := hmac.New(sha512.New384, randBytes) + if _, err := macN.Write(payloadBytes); err != nil { + return "", fmt.Errorf("hash payload: %w", err) + } + nonce := macN.Sum(nil)[:v1LocNonceSize] + + encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf]) + if err != nil { + return "", fmt.Errorf("create enc and auth keys: %w", err) + } + + block, err := aes.NewCipher(encKey) + if err != nil { + return "", fmt.Errorf("create aes cipher: %w", err) + } + + encryptedPayload := make([]byte, len(payloadBytes)) + cipher.NewCTR(block, nonce[v1LocNonceHalf:]). + XORKeyStream(encryptedPayload, payloadBytes) + + h := hmac.New(sha512.New384, authKey) + if _, err := h.Write(pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes)); err != nil { + return "", fmt.Errorf("create signature: %w", err) + } + mac := h.Sum(nil) + + body := make([]byte, 0, len(nonce)+len(encryptedPayload)+len(mac)) + body = append(body, nonce...) + body = append(body, encryptedPayload...) + body = append(body, mac...) + + return buildToken(v1LocHeader, body, footerBytes), nil +} + +func V1Decrypt(token string, key []byte, payload, footer any) error { + data, footerBytes, err := splitToken(token, v1LocHeader) + if err != nil { + return fmt.Errorf("decode token: %w", err) + } + if len(data) < v1LocNonceSize+v1LocMacSize { + return ErrIncorrectTokenFormat + } + + pivot := len(data) - v1LocMacSize + nonce := data[:v1LocNonceSize] + encryptedPayload, mac := data[v1LocNonceSize:pivot], data[pivot:] + + encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf]) + if err != nil { + return fmt.Errorf("create enc and auth keys: %w", err) + } + + body := pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes) + h := hmac.New(sha512.New384, authKey) + if _, err := h.Write(body); err != nil { + return fmt.Errorf("create signature: %w", err) + } + + if !hmac.Equal(h.Sum(nil), mac) { + return fmt.Errorf("token signature: %w", ErrInvalidTokenAuth) + } + + block, err := aes.NewCipher(encKey) + if err != nil { + return fmt.Errorf("create aes cipher: %w", err) + } + + decryptedPayload := make([]byte, len(encryptedPayload)) + cipher.NewCTR(block, nonce[v1LocNonceHalf:]). + XORKeyStream(decryptedPayload, encryptedPayload) + + 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 +} + +func v1locSplitKey(key, salt []byte) ([]byte, []byte, error) { + eReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-encryption-key")) + aReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-auth-key-for-aead")) + + encKey := make([]byte, 32) + authKey := make([]byte, 32) + + if _, err := io.ReadFull(eReader, encKey); err != nil { + return nil, nil, err + } + if _, err := io.ReadFull(aReader, authKey); err != nil { + return nil, nil, err + } + return encKey, authKey, nil +} diff --git a/v1loc_test.go b/v1loc_test.go new file mode 100644 index 0000000..7c284ef --- /dev/null +++ b/v1loc_test.go @@ -0,0 +1,48 @@ +package paseto + +import ( + "encoding/hex" + "strings" + "testing" +) + +func TestV1Loc_Encrypt(t *testing.T) { + testCases := loadGoldenFile("testdata/v1.json") + + for _, tc := range testCases.Tests { + if tc.Key == "" || !strings.HasPrefix(tc.Token, v1LocHeader) { + continue + } + + t.Run(tc.Name, func(t *testing.T) { + key := must(hex.DecodeString(tc.Key)) + payload := mustJSON(tc.Payload) + footer := mustJSON(tc.Footer) + nonce := must(hex.DecodeString(tc.Nonce)) + + token, err := V1Encrypt(key, payload, footer, nonce) + if err != nil { + t.Fatal(err) + } + mustEqual(t, token, tc.Token) + }) + } +} + +func TestV1Loc_Decrypt(t *testing.T) { + testCases := loadGoldenFile("testdata/v1.json") + + for _, tc := range testCases.Tests[:] { + if tc.Key == "" || !strings.HasPrefix(tc.Token, v1LocHeader) { + continue + } + + t.Run(tc.Name, func(t *testing.T) { + key := must(hex.DecodeString(tc.Key)) + var payload, footer any + + err := V1Decrypt(tc.Token, key, payload, footer) + mustOk(t, err) + }) + } +}