diff options
| -rw-r--r-- | CHANGELOG.adoc | 10 | ||||
| -rw-r--r-- | lib/paseto/paseto.go | 330 | ||||
| -rw-r--r-- | lib/paseto/paseto_test.go | 269 | ||||
| -rw-r--r-- | lib/paseto/v2/example_local_mode_test.go (renamed from lib/paseto/example_local_mode_test.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/example_public_mode_test.go (renamed from lib/paseto/example_public_mode_test.go) | 5 | ||||
| -rw-r--r-- | lib/paseto/v2/json_footer.go (renamed from lib/paseto/json_footer.go) | 5 | ||||
| -rw-r--r-- | lib/paseto/v2/json_token.go (renamed from lib/paseto/json_token.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/json_token_test.go (renamed from lib/paseto/json_token_test.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/key.go (renamed from lib/paseto/key.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/keys.go (renamed from lib/paseto/keys.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/local_mode.go (renamed from lib/paseto/local_mode.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/paseto.go | 305 | ||||
| -rw-r--r-- | lib/paseto/v2/paseto_test.go | 264 | ||||
| -rw-r--r-- | lib/paseto/v2/public_mode.go (renamed from lib/paseto/public_mode.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/public_mode_test.go (renamed from lib/paseto/public_mode_test.go) | 2 | ||||
| -rw-r--r-- | lib/paseto/v2/public_token.go (renamed from lib/paseto/public_token.go) | 2 |
16 files changed, 615 insertions, 591 deletions
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 43b4c85f..2270a2f2 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -84,6 +84,16 @@ the worker for keeping the connection alive also call Write at the same time, which cause the data race. +[#v0_62_0__lib_paseto] +=== lib/paseto + +==== 🪵 Move paseto v2 under sub directory "v2" + +There are new versions of paseto standard: version 3 and version 4. +To minimize conflicts in the future, we move the old implementation of +paseto v2 to sub directory "v2" with package name "pasetov2". + + [#v0_62_0__lib_uuidv7] === lib/uuidv7 diff --git a/lib/paseto/paseto.go b/lib/paseto/paseto.go index e37bcb2e..f903fd79 100644 --- a/lib/paseto/paseto.go +++ b/lib/paseto/paseto.go @@ -1,342 +1,44 @@ -// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> -// // SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> // Package paseto provide a simple, ready to use, opinionated implementation -// of Platform-Agnostic SEcurity TOkens (PASETOs) v2 as defined in -// [paseto-rfc-01]. -// -// See the examples below for quick reference. -// -// # Limitation -// -// This implementation only support PASETO Protocol v2. -// -// # Local mode -// -// The local mode use crypto/rand package to generate random nonce and hashed -// with blake2b. -// -// # Public mode -// -// The public mode focus on signing and verifing data, everything else is -// handled and filled automatically. -// -// Steps for sender when generating new token, the Pack() method, -// -// (1) Prepare the JSON token claims, set -// -// - Issuer "iss" to PublicMode.our.ID -// - Subject "sub" to subject value from parameter -// - Audience "aud" to audience value from parameter -// - IssuedAt to current time -// - NotBefore to current time -// - ExpiredAt to current time + 60 seconds -// - Data field to base64 encoded of data value from parameter -// -// (2) Prepare the JSON footer, set -// -// - Key ID "kid" to PublicMode.our.ID -// -// The user's claims data is stored using key "data" inside the JSON token, -// encoded using base64 (with padding). -// Additional footer data can be added on the Data field. +// of Platform-Agnostic SEcurity TOkens (PASETO) version 2 and version 4 as +// defined in [paseto-v2] and [paseto-v4]. // -// Overall, the following JSONToken and JSONFooter is generated for each -// token, +// The paseto version 2 is available under sub packet +// [git.sr.ht/~shulhan/pakakeh.go/lib/paseto/v2] and the paseto version 4 +// is available under sub packet +// [git.sr.ht/~shulhan/pakakeh.go/lib/paseto/v4]. // -// JSONToken:{ -// "iss": <Key.ID>, -// "sub": <Subject parameter>, -// "aud": <Audience parameter> -// "exp": <time.Now() + TTL>, -// "iat": <time.Now()>, -// "nbf": <time.Now()>, -// "data": <base64.StdEncoding.EncodeToString(userData)>, -// } -// JSONFooter:{ -// "kid": <Key.ID>, -// "data": {} -// } -// -// On the receiver side, they will have list of registered peers Key (include -// ID, public Key, and list of allowed subject). -// -// PublicMode:{ -// peers: map[Key.ID]Key{ -// Public: <ed25519.PublicKey>, -// AllowedSubjects: map[string]struct{}{ -// "/api/x": struct{}{}, -// "/api/y:read": struct{}{}, -// "/api/z:write": struct{}{}, -// ... -// }, -// }, -// } -// -// Step for receiver to process the token, the Unpack() method, -// -// (1) Decode the token footer -// -// (2) Get the registered public key based on "kid" value in token footer. -// If no peers key exist matched with "kid" value, reject the token. -// -// (3) Verify the token using the peer public key. -// If verification failed, reject the token. -// -// (4) Validate the token. -// - The Issuer must equal to peer ID. -// - The Audience must equal to receiver ID. -// - If the peer AllowedSubjects is not empty, the Subject must be in -// one of them. -// - The current time must be after IssuedAt. -// - The current time must be after NotBefore. -// - The current time must be before ExpiredAt. -// - If one of the above condition is not passed, it will return an error. -// -// # References -// -// - [paseto-rfc-01] -// -// [paseto-rfc-01]: https://github.com/paragonie/paseto/blob/master/docs/RFC/draft-paragon-paseto-rfc-01.txt +// [paseto-v2]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version2.md +// [paseto-v4]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md package paseto import ( "bytes" - "crypto/cipher" - "crypto/ed25519" - "crypto/rand" - "encoding/base64" "encoding/binary" - "errors" - "strings" - - "golang.org/x/crypto/blake2b" -) - -const ( - randNonceSize = 24 -) - -var ( - headerModePublic = []byte("v2.public.") - headerModeLocal = []byte("v2.local.") + "fmt" ) -// Encrypt given the shared key, encrypt the plain message and generate the -// "local" token with optional footer. -func Encrypt(aead cipher.AEAD, plain, footer []byte) (token string, err error) { - nonce := make([]byte, randNonceSize) - _, err = rand.Read(nonce) - if err != nil { - return "", err - } - - return encrypt(aead, nonce, plain, footer) -} - -func encrypt(aead cipher.AEAD, nonce, plain, footer []byte) (token string, err error) { - b2b, err := blake2b.New(randNonceSize, nonce) - if err != nil { - return "", err - } - - _, err = b2b.Write(plain) - if err != nil { - return "", err - } - - nonce = b2b.Sum(nil) - - pieces := [][]byte{headerModeLocal, nonce, footer} - - m2, err := pae(pieces) - if err != nil { - return "", err - } - - cipher := aead.Seal(nil, nonce, plain, m2) - - var buf bytes.Buffer - - _, err = buf.Write(headerModeLocal) - if err != nil { - return "", err - } - - sc := make([]byte, 0, len(nonce)+len(cipher)) - sc = append(sc, nonce...) - sc = append(sc, cipher...) - - n := base64.RawURLEncoding.EncodedLen(len(sc)) - dst := make([]byte, n) - base64.RawURLEncoding.Encode(dst, sc) - _, err = buf.Write(dst) - if err != nil { - return ``, err - } - - if len(footer) > 0 { - buf.WriteByte('.') - - n = base64.RawURLEncoding.EncodedLen(len(footer)) - dst = make([]byte, n) - base64.RawURLEncoding.Encode(dst, footer) - _, err = buf.Write(dst) - if err != nil { - return ``, err - } - } - - return buf.String(), nil -} - -// Decrypt given a shared key and encrypted token, decrypt the token to get -// the message. -func Decrypt(aead cipher.AEAD, token string) (plain, footer []byte, err error) { - pieces := strings.Split(token, ".") - if len(pieces) < 3 || len(pieces) > 4 { - return nil, nil, errors.New("invalid token format") - } - if pieces[0] != "v2" { - return nil, nil, errors.New(`unsupported protocol version ` + pieces[0]) - } - if pieces[1] != "local" { - return nil, nil, errors.New(`expecting local mode, got ` + pieces[1]) - } - - if len(pieces) == 4 { - footer, err = base64.RawURLEncoding.DecodeString(pieces[3]) - if err != nil { - return nil, nil, err - } - } - - src, err := base64.RawURLEncoding.DecodeString(pieces[2]) - if err != nil { - return nil, nil, err - } - - nonce := src[:randNonceSize] - cipher := src[randNonceSize:] - - if len(cipher) < aead.NonceSize() { - return nil, nil, errors.New("ciphertext too short") - } - - m2, err := pae([][]byte{headerModeLocal, nonce, footer}) - if err != nil { - return nil, nil, err - } - - plain, err = aead.Open(nil, nonce, cipher, m2) - if err != nil { - return nil, nil, err - } - - return plain, footer, nil -} - -// Sign given an Ed25519 secret key "sk", a message "m", and optional footer -// "f" (which defaults to empty string); sign the message "m" and generate the -// public token. -func Sign(sk ed25519.PrivateKey, m, f []byte) (token string, err error) { - pieces := [][]byte{headerModePublic, m, f} - - m2, err := pae(pieces) - if err != nil { - return "", err - } - - sig := ed25519.Sign(sk, m2) - - var buf bytes.Buffer - - _, err = buf.Write(headerModePublic) - if err != nil { - return "", err - } - - sm := make([]byte, 0, len(m)+len(sig)) - sm = append(sm, m...) - sm = append(sm, sig...) - - n := base64.RawURLEncoding.EncodedLen(len(sm)) - dst := make([]byte, n) - base64.RawURLEncoding.Encode(dst, sm) - - _, err = buf.Write(dst) - if err != nil { - return "", err - } - - if len(f) > 0 { - _ = buf.WriteByte('.') - - n = base64.RawURLEncoding.EncodedLen(len(f)) - dst = make([]byte, n) - base64.RawURLEncoding.Encode(dst, f) - - _, err = buf.Write(dst) - if err != nil { - return "", err - } - } - - return buf.String(), nil -} - -// Verify given a public key "pk", a signed message "sm" (that has been -// decoded from base64), and optional footer "f" (also that has been decoded -// from base64 string); verify that the signature is valid for the message. -func Verify(pk ed25519.PublicKey, sm, f []byte) (msg []byte, err error) { - if len(sm) <= 64 { - return nil, errors.New(`invalid signed message length`) - } - - msg = sm[:len(sm)-64] - sig := sm[len(sm)-64:] - pieces := [][]byte{headerModePublic, msg, f} - - msg2, err := pae(pieces) - if err != nil { - return nil, err - } - - if !ed25519.Verify(pk, msg2, sig) { - return nil, errors.New(`invalid message signature`) - } - - return msg, nil -} - -func pae(pieces [][]byte) (b []byte, err error) { +// PreAuthEncode encodes each piece into single block. +func PreAuthEncode(pieces ...[]byte) (b []byte, err error) { + logp := `PreAuthEncode` var buf bytes.Buffer err = binary.Write(&buf, binary.LittleEndian, uint64(len(pieces))) if err != nil { - return nil, err - } - - _, err = buf.Write(b) - if err != nil { - return nil, err + return nil, fmt.Errorf(`%s: %w`, logp, err) } for x := range len(pieces) { err = binary.Write(&buf, binary.LittleEndian, uint64(len(pieces[x]))) if err != nil { - return nil, err - } - - _, err = buf.Write(b) - if err != nil { - return nil, err + return nil, fmt.Errorf(`%s: %w`, logp, err) } _, err = buf.Write(pieces[x]) if err != nil { - return nil, err + return nil, fmt.Errorf(`%s: %w`, logp, err) } } return buf.Bytes(), nil diff --git a/lib/paseto/paseto_test.go b/lib/paseto/paseto_test.go index 5c267bc8..4a096dfd 100644 --- a/lib/paseto/paseto_test.go +++ b/lib/paseto/paseto_test.go @@ -1,294 +1,39 @@ // SPDX-License-Identifier: BSD-3-Clause -// SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> package paseto import ( - "encoding/base64" - "encoding/hex" - "strings" "testing" "git.sr.ht/~shulhan/pakakeh.go/lib/test" - "golang.org/x/crypto/chacha20poly1305" ) -func TestPae(t *testing.T) { +func TestPreAuthEncode(t *testing.T) { cases := []struct { pieces [][]byte exp []byte }{{ exp: []byte("\x00\x00\x00\x00\x00\x00\x00\x00"), }, { - pieces: [][]byte{[]byte{}}, + pieces: [][]byte{[]byte(``)}, exp: []byte("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), }, { pieces: [][]byte{{}, {}}, exp: []byte("\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), }, { - pieces: [][]byte{[]byte("test")}, + pieces: [][]byte{[]byte(`test`)}, exp: []byte("\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00test"), }, { - pieces: [][]byte{[]byte("Paragon")}, + pieces: [][]byte{[]byte(`Paragon`)}, exp: []byte("\x01\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x50\x61\x72\x61\x67\x6f\x6e"), }} for _, c := range cases { - got, err := pae(c.pieces) + got, err := PreAuthEncode(c.pieces...) if err != nil { t.Fatal(err) } - - test.Assert(t, "pae", c.exp, got) - } -} - -func TestEncrypt(t *testing.T) { - hexKey := "70717273" + "74757677" + "78797a7b" + "7c7d7e7f" + - "80818283" + "84858687" + "88898a8b" + "8c8d8e8f" - - key, err := hex.DecodeString(hexKey) - if err != nil { - t.Fatal(err) - } - - aead, err := chacha20poly1305.NewX(key) - if err != nil { - t.Fatal(err) - } - - cases := []struct { - desc string - nonce string - exp string - - msg []byte - footer []byte - }{{ - desc: "Encrypt with zero nonce, without footer", - msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), - nonce: "00000000" + "00000000" + "00000000" + "00000000" + - "00000000" + "00000000", - exp: "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4Pn" + - "W8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVOD" + - "yfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", - }, { - desc: "Encrypt with zero nonce, without footer (2)", - msg: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), - nonce: "00000000" + "00000000" + "00000000" + "00000000" + - "00000000" + "00000000", - exp: "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg" + - "3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7" + - "J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", - }, { - desc: "Encrypt with nonce, without footer", - msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), - nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + - "cda2f64c" + "84fda19b", - exp: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + - "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + - "Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", - }, { - desc: "Encrypt with nonce, with footer", - msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), - nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + - "cda2f64c" + "84fda19b", - footer: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), - exp: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + - "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + - "Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlm" + - "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - }, { - desc: "Encrypt with nonce, with footer (2)", - msg: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), - nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + - "cda2f64c" + "84fda19b", - footer: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), - exp: "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7" + - "cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUr" + - "Iu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlm" + - "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - }} - - for _, c := range cases { - nonce, err := hex.DecodeString(c.nonce) - if err != nil { - t.Fatal(err) - } - - got, err := encrypt(aead, nonce, c.msg, c.footer) - if err != nil { - t.Fatal(err) - } - - test.Assert(t, c.desc, c.exp, got) - } -} - -func TestDecrypt(t *testing.T) { - hexKey := "70717273" + "74757677" + "78797a7b" + "7c7d7e7f" + - "80818283" + "84858687" + "88898a8b" + "8c8d8e8f" - - key, err := hex.DecodeString(hexKey) - if err != nil { - t.Fatal(err) - } - - aead, err := chacha20poly1305.NewX(key) - if err != nil { - t.Fatal(err) - } - - cases := []struct { - desc string - token string - exp []byte - expFooter []byte - }{{ - desc: "Decrypt without nonce and footer", - token: "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4Pn" + - "W8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVOD" + - "yfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", - exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), - }, { - desc: "Decrypt without nonce and footer (2)", - token: "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg" + - "3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7" + - "J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", - exp: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), - }, { - desc: "Decrypt with nonce, without footer", - token: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + - "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + - "Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", - exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), - }, { - desc: "Decrypt with nonce, with footer", - token: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + - "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + - "Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlm" + - "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), - expFooter: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), - }, { - desc: "Decrypt with nonce, with footer (2)", - token: "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7" + - "cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUr" + - "Iu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlm" + - "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - exp: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), - expFooter: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), - }} - - for _, c := range cases { - got, gotFooter, err := Decrypt(aead, c.token) - if err != nil { - t.Fatal(err) - } - - test.Assert(t, c.desc, c.exp, got) - test.Assert(t, c.desc, c.expFooter, gotFooter) - } -} - -func TestSign(t *testing.T) { - hexPrivate := "b4cbfb43" + "df4ce210" + "727d953e" + "4a713307" + - "fa19bb7d" + "9f850414" + "38d9e11b" + "942a3774" + - "1eb9dbbb" + "bc047c03" + "fd70604e" + "0071f098" + - "7e16b28b" + "757225c1" + "1f00415d" + "0e20b1a2" - - sk, err := hex.DecodeString(hexPrivate) - if err != nil { - t.Fatal() - } - - m := []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`) - - cases := []struct { - desc string - exp string - - m []byte - f []byte - }{{ - desc: "Sign", - m: m, - exp: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + - "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt" + - "Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj" + - "JK2ZXC2SUYuOFM-Q_5Cw", - }, { - desc: "Sign with footer", - m: m, - f: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), - exp: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + - "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC" + - "R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601" + - "tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q" + - "3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - }} - - for _, c := range cases { - got, err := Sign(sk, c.m, c.f) - if err != nil { - t.Fatal(err) - } - - test.Assert(t, c.desc, c.exp, got) - } -} - -func TestVerify(t *testing.T) { - hexPublic := "1eb9dbbb" + "bc047c03" + "fd70604e" + "0071f098" + - "7e16b28b" + "757225c1" + "1f00415d" + "0e20b1a2" - - public, err := hex.DecodeString(hexPublic) - if err != nil { - t.Fatal() - } - - cases := []struct { - desc string - token string - exp string - }{{ - desc: "Verify", - token: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + - "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt" + - "Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj" + - "JK2ZXC2SUYuOFM-Q_5Cw", - exp: `{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`, - }, { - desc: "Verify with footer", - token: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + - "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC" + - "R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601" + - "tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q" + - "3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - exp: `{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`, - }} - - for _, c := range cases { - var footer []byte - - pieces := strings.Split(c.token, ".") - - sm, err := base64.RawURLEncoding.DecodeString(pieces[2]) - if err != nil { - t.Fatal(err) - } - if len(pieces) == 4 { - footer, err = base64.RawURLEncoding.DecodeString(pieces[3]) - if err != nil { - t.Fatal(err) - } - } - - got, err := Verify(public, sm, footer) - if err != nil { - t.Fatal(err) - } - - test.Assert(t, c.desc, c.exp, string(got)) + test.Assert(t, `PreAuthEncode`, c.exp, got) } } diff --git a/lib/paseto/example_local_mode_test.go b/lib/paseto/v2/example_local_mode_test.go index 3046f8c6..15960e4a 100644 --- a/lib/paseto/example_local_mode_test.go +++ b/lib/paseto/v2/example_local_mode_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import ( "encoding/hex" diff --git a/lib/paseto/example_public_mode_test.go b/lib/paseto/v2/example_public_mode_test.go index a821c93b..0930aebf 100644 --- a/lib/paseto/example_public_mode_test.go +++ b/lib/paseto/v2/example_public_mode_test.go @@ -1,8 +1,7 @@ -// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> -// // SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import ( "crypto/ed25519" diff --git a/lib/paseto/json_footer.go b/lib/paseto/v2/json_footer.go index 990ee688..67a97444 100644 --- a/lib/paseto/json_footer.go +++ b/lib/paseto/v2/json_footer.go @@ -1,8 +1,7 @@ -// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> -// // SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> -package paseto +package pasetov2 // JSONFooter define the optional metadata and data at the footer of the // token that are not included in signature. diff --git a/lib/paseto/json_token.go b/lib/paseto/v2/json_token.go index f5ab4979..a3b6006d 100644 --- a/lib/paseto/json_token.go +++ b/lib/paseto/v2/json_token.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import ( "fmt" diff --git a/lib/paseto/json_token_test.go b/lib/paseto/v2/json_token_test.go index 1d43e058..5081ebf4 100644 --- a/lib/paseto/json_token_test.go +++ b/lib/paseto/v2/json_token_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import ( "fmt" diff --git a/lib/paseto/key.go b/lib/paseto/v2/key.go index b5a2a083..fdd51988 100644 --- a/lib/paseto/key.go +++ b/lib/paseto/v2/key.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import "crypto/ed25519" diff --git a/lib/paseto/keys.go b/lib/paseto/v2/keys.go index 170efab0..bfe7e97f 100644 --- a/lib/paseto/keys.go +++ b/lib/paseto/v2/keys.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import "sync" diff --git a/lib/paseto/local_mode.go b/lib/paseto/v2/local_mode.go index cfab3b3d..0cc29aef 100644 --- a/lib/paseto/local_mode.go +++ b/lib/paseto/v2/local_mode.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import ( "crypto/cipher" diff --git a/lib/paseto/v2/paseto.go b/lib/paseto/v2/paseto.go new file mode 100644 index 00000000..a7fb7f77 --- /dev/null +++ b/lib/paseto/v2/paseto.go @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> + +// Package paseto provide a simple, ready to use, opinionated implementation +// of Platform-Agnostic SEcurity TOkens (PASETOs) v2 as defined in +// [paseto-rfc-01]. +// +// See the examples below for quick reference. +// +// # Limitation +// +// This implementation only support PASETO Protocol v2. +// +// # Local mode +// +// The local mode use crypto/rand package to generate random nonce and hashed +// with blake2b. +// +// # Public mode +// +// The public mode focus on signing and verifing data, everything else is +// handled and filled automatically. +// +// Steps for sender when generating new token, the Pack() method, +// +// (1) Prepare the JSON token claims, set +// +// - Issuer "iss" to PublicMode.our.ID +// - Subject "sub" to subject value from parameter +// - Audience "aud" to audience value from parameter +// - IssuedAt to current time +// - NotBefore to current time +// - ExpiredAt to current time + 60 seconds +// - Data field to base64 encoded of data value from parameter +// +// (2) Prepare the JSON footer, set +// +// - Key ID "kid" to PublicMode.our.ID +// +// The user's claims data is stored using key "data" inside the JSON token, +// encoded using base64 (with padding). +// Additional footer data can be added on the Data field. +// +// Overall, the following JSONToken and JSONFooter is generated for each +// token, +// +// JSONToken:{ +// "iss": <Key.ID>, +// "sub": <Subject parameter>, +// "aud": <Audience parameter> +// "exp": <time.Now() + TTL>, +// "iat": <time.Now()>, +// "nbf": <time.Now()>, +// "data": <base64.StdEncoding.EncodeToString(userData)>, +// } +// JSONFooter:{ +// "kid": <Key.ID>, +// "data": {} +// } +// +// On the receiver side, they will have list of registered peers Key (include +// ID, public Key, and list of allowed subject). +// +// PublicMode:{ +// peers: map[Key.ID]Key{ +// Public: <ed25519.PublicKey>, +// AllowedSubjects: map[string]struct{}{ +// "/api/x": struct{}{}, +// "/api/y:read": struct{}{}, +// "/api/z:write": struct{}{}, +// ... +// }, +// }, +// } +// +// Step for receiver to process the token, the Unpack() method, +// +// (1) Decode the token footer +// +// (2) Get the registered public key based on "kid" value in token footer. +// If no peers key exist matched with "kid" value, reject the token. +// +// (3) Verify the token using the peer public key. +// If verification failed, reject the token. +// +// (4) Validate the token. +// - The Issuer must equal to peer ID. +// - The Audience must equal to receiver ID. +// - If the peer AllowedSubjects is not empty, the Subject must be in +// one of them. +// - The current time must be after IssuedAt. +// - The current time must be after NotBefore. +// - The current time must be before ExpiredAt. +// - If one of the above condition is not passed, it will return an error. +// +// # References +// +// - [paseto-rfc-01] +// +// [paseto-rfc-01]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version2.md +package pasetov2 + +import ( + "bytes" + "crypto/cipher" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "errors" + "strings" + + "git.sr.ht/~shulhan/pakakeh.go/lib/paseto" + "golang.org/x/crypto/blake2b" +) + +const ( + randNonceSize = 24 +) + +var ( + headerModePublic = []byte("v2.public.") + headerModeLocal = []byte("v2.local.") +) + +// Encrypt given the shared key, encrypt the plain message and generate the +// "local" token with optional footer. +func Encrypt(aead cipher.AEAD, plain, footer []byte) (token string, err error) { + nonce := make([]byte, randNonceSize) + _, err = rand.Read(nonce) + if err != nil { + return "", err + } + + return encrypt(aead, nonce, plain, footer) +} + +func encrypt(aead cipher.AEAD, nonce, plain, footer []byte) (token string, err error) { + b2b, err := blake2b.New(randNonceSize, nonce) + if err != nil { + return "", err + } + + _, err = b2b.Write(plain) + if err != nil { + return "", err + } + + nonce = b2b.Sum(nil) + + m2, err := paseto.PreAuthEncode(headerModeLocal, nonce, footer) + if err != nil { + return "", err + } + + cipher := aead.Seal(nil, nonce, plain, m2) + + var buf bytes.Buffer + + _, err = buf.Write(headerModeLocal) + if err != nil { + return "", err + } + + sc := make([]byte, 0, len(nonce)+len(cipher)) + sc = append(sc, nonce...) + sc = append(sc, cipher...) + + n := base64.RawURLEncoding.EncodedLen(len(sc)) + dst := make([]byte, n) + base64.RawURLEncoding.Encode(dst, sc) + _, err = buf.Write(dst) + if err != nil { + return ``, err + } + + if len(footer) > 0 { + buf.WriteByte('.') + + n = base64.RawURLEncoding.EncodedLen(len(footer)) + dst = make([]byte, n) + base64.RawURLEncoding.Encode(dst, footer) + _, err = buf.Write(dst) + if err != nil { + return ``, err + } + } + + return buf.String(), nil +} + +// Decrypt given a shared key and encrypted token, decrypt the token to get +// the message. +func Decrypt(aead cipher.AEAD, token string) (plain, footer []byte, err error) { + pieces := strings.Split(token, ".") + if len(pieces) < 3 || len(pieces) > 4 { + return nil, nil, errors.New("invalid token format") + } + if pieces[0] != "v2" { + return nil, nil, errors.New(`unsupported protocol version ` + pieces[0]) + } + if pieces[1] != "local" { + return nil, nil, errors.New(`expecting local mode, got ` + pieces[1]) + } + + if len(pieces) == 4 { + footer, err = base64.RawURLEncoding.DecodeString(pieces[3]) + if err != nil { + return nil, nil, err + } + } + + src, err := base64.RawURLEncoding.DecodeString(pieces[2]) + if err != nil { + return nil, nil, err + } + + nonce := src[:randNonceSize] + cipher := src[randNonceSize:] + + if len(cipher) < aead.NonceSize() { + return nil, nil, errors.New("ciphertext too short") + } + + m2, err := paseto.PreAuthEncode(headerModeLocal, nonce, footer) + if err != nil { + return nil, nil, err + } + + plain, err = aead.Open(nil, nonce, cipher, m2) + if err != nil { + return nil, nil, err + } + + return plain, footer, nil +} + +// Sign given an Ed25519 secret key "sk", a message "m", and optional footer +// "f" (which defaults to empty string); sign the message "m" and generate the +// public token. +func Sign(sk ed25519.PrivateKey, m, f []byte) (token string, err error) { + m2, err := paseto.PreAuthEncode(headerModePublic, m, f) + if err != nil { + return "", err + } + + sig := ed25519.Sign(sk, m2) + + var buf bytes.Buffer + + _, err = buf.Write(headerModePublic) + if err != nil { + return "", err + } + + sm := make([]byte, 0, len(m)+len(sig)) + sm = append(sm, m...) + sm = append(sm, sig...) + + n := base64.RawURLEncoding.EncodedLen(len(sm)) + dst := make([]byte, n) + base64.RawURLEncoding.Encode(dst, sm) + + _, err = buf.Write(dst) + if err != nil { + return "", err + } + + if len(f) > 0 { + _ = buf.WriteByte('.') + + n = base64.RawURLEncoding.EncodedLen(len(f)) + dst = make([]byte, n) + base64.RawURLEncoding.Encode(dst, f) + + _, err = buf.Write(dst) + if err != nil { + return "", err + } + } + + return buf.String(), nil +} + +// Verify given a public key "pk", a signed message "sm" (that has been +// decoded from base64), and optional footer "f" (also that has been decoded +// from base64 string); verify that the signature is valid for the message. +func Verify(pk ed25519.PublicKey, sm, f []byte) (msg []byte, err error) { + if len(sm) <= 64 { + return nil, errors.New(`invalid signed message length`) + } + + msg = sm[:len(sm)-64] + sig := sm[len(sm)-64:] + + msg2, err := paseto.PreAuthEncode(headerModePublic, msg, f) + if err != nil { + return nil, err + } + + if !ed25519.Verify(pk, msg2, sig) { + return nil, errors.New(`invalid message signature`) + } + + return msg, nil +} diff --git a/lib/paseto/v2/paseto_test.go b/lib/paseto/v2/paseto_test.go new file mode 100644 index 00000000..7e066456 --- /dev/null +++ b/lib/paseto/v2/paseto_test.go @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> + +package pasetov2 + +import ( + "encoding/base64" + "encoding/hex" + "strings" + "testing" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test" + "golang.org/x/crypto/chacha20poly1305" +) + +func TestEncrypt(t *testing.T) { + hexKey := "70717273" + "74757677" + "78797a7b" + "7c7d7e7f" + + "80818283" + "84858687" + "88898a8b" + "8c8d8e8f" + + key, err := hex.DecodeString(hexKey) + if err != nil { + t.Fatal(err) + } + + aead, err := chacha20poly1305.NewX(key) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + desc string + nonce string + exp string + + msg []byte + footer []byte + }{{ + desc: "Encrypt with zero nonce, without footer", + msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "00000000" + "00000000" + "00000000" + "00000000" + + "00000000" + "00000000", + exp: "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4Pn" + + "W8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVOD" + + "yfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", + }, { + desc: "Encrypt with zero nonce, without footer (2)", + msg: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "00000000" + "00000000" + "00000000" + "00000000" + + "00000000" + "00000000", + exp: "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg" + + "3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7" + + "J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", + }, { + desc: "Encrypt with nonce, without footer", + msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + + "cda2f64c" + "84fda19b", + exp: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", + }, { + desc: "Encrypt with nonce, with footer", + msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + + "cda2f64c" + "84fda19b", + footer: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + exp: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + }, { + desc: "Encrypt with nonce, with footer (2)", + msg: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + + "cda2f64c" + "84fda19b", + footer: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + exp: "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7" + + "cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUr" + + "Iu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + }} + + for _, c := range cases { + nonce, err := hex.DecodeString(c.nonce) + if err != nil { + t.Fatal(err) + } + + got, err := encrypt(aead, nonce, c.msg, c.footer) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, got) + } +} + +func TestDecrypt(t *testing.T) { + hexKey := "70717273" + "74757677" + "78797a7b" + "7c7d7e7f" + + "80818283" + "84858687" + "88898a8b" + "8c8d8e8f" + + key, err := hex.DecodeString(hexKey) + if err != nil { + t.Fatal(err) + } + + aead, err := chacha20poly1305.NewX(key) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + desc string + token string + exp []byte + expFooter []byte + }{{ + desc: "Decrypt without nonce and footer", + token: "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4Pn" + + "W8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVOD" + + "yfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", + exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + }, { + desc: "Decrypt without nonce and footer (2)", + token: "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg" + + "3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7" + + "J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", + exp: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + }, { + desc: "Decrypt with nonce, without footer", + token: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", + exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + }, { + desc: "Decrypt with nonce, with footer", + token: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + expFooter: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + }, { + desc: "Decrypt with nonce, with footer (2)", + token: "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7" + + "cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUr" + + "Iu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + exp: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + expFooter: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + }} + + for _, c := range cases { + got, gotFooter, err := Decrypt(aead, c.token) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, got) + test.Assert(t, c.desc, c.expFooter, gotFooter) + } +} + +func TestSign(t *testing.T) { + hexPrivate := "b4cbfb43" + "df4ce210" + "727d953e" + "4a713307" + + "fa19bb7d" + "9f850414" + "38d9e11b" + "942a3774" + + "1eb9dbbb" + "bc047c03" + "fd70604e" + "0071f098" + + "7e16b28b" + "757225c1" + "1f00415d" + "0e20b1a2" + + sk, err := hex.DecodeString(hexPrivate) + if err != nil { + t.Fatal() + } + + m := []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`) + + cases := []struct { + desc string + exp string + + m []byte + f []byte + }{{ + desc: "Sign", + m: m, + exp: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt" + + "Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj" + + "JK2ZXC2SUYuOFM-Q_5Cw", + }, { + desc: "Sign with footer", + m: m, + f: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + exp: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC" + + "R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601" + + "tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q" + + "3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + }} + + for _, c := range cases { + got, err := Sign(sk, c.m, c.f) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, got) + } +} + +func TestVerify(t *testing.T) { + hexPublic := "1eb9dbbb" + "bc047c03" + "fd70604e" + "0071f098" + + "7e16b28b" + "757225c1" + "1f00415d" + "0e20b1a2" + + public, err := hex.DecodeString(hexPublic) + if err != nil { + t.Fatal() + } + + cases := []struct { + desc string + token string + exp string + }{{ + desc: "Verify", + token: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt" + + "Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj" + + "JK2ZXC2SUYuOFM-Q_5Cw", + exp: `{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`, + }, { + desc: "Verify with footer", + token: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC" + + "R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601" + + "tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q" + + "3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + exp: `{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`, + }} + + for _, c := range cases { + var footer []byte + + pieces := strings.Split(c.token, ".") + + sm, err := base64.RawURLEncoding.DecodeString(pieces[2]) + if err != nil { + t.Fatal(err) + } + if len(pieces) == 4 { + footer, err = base64.RawURLEncoding.DecodeString(pieces[3]) + if err != nil { + t.Fatal(err) + } + } + + got, err := Verify(public, sm, footer) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, string(got)) + } +} diff --git a/lib/paseto/public_mode.go b/lib/paseto/v2/public_mode.go index b43b56d3..98d7d2da 100644 --- a/lib/paseto/public_mode.go +++ b/lib/paseto/v2/public_mode.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: BSD-3-Clause -package paseto +package pasetov2 import ( "encoding/base64" diff --git a/lib/paseto/public_mode_test.go b/lib/paseto/v2/public_mode_test.go index 8b8e7c00..2967b4bd 100644 --- a/lib/paseto/public_mode_test.go +++ b/lib/paseto/v2/public_mode_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> -package paseto +package pasetov2 import ( "crypto/ed25519" diff --git a/lib/paseto/public_token.go b/lib/paseto/v2/public_token.go index 33f28d15..446eb12b 100644 --- a/lib/paseto/public_token.go +++ b/lib/paseto/v2/public_token.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020 Shulhan <ms@kilabit.info> -package paseto +package pasetov2 // PublicToken contains the unpacked public token. type PublicToken struct { |
