aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2026-03-28 18:15:05 +0700
committerShulhan <ms@kilabit.info>2026-03-28 20:06:41 +0700
commitf3011a9cdfc59b72ce0203c48169e97d21235057 (patch)
tree6829034269dbf511ba4dcb8ebde9474db6079d79
parentdf7fcb9796d330e7151761444a803f9ae3cc5011 (diff)
downloadpakakeh.go-f3011a9cdfc59b72ce0203c48169e97d21235057.tar.xz
lib/paseto: move implementation to 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". The paseto package now left with common functions, like creating pre-authentication encoding (PAE).
-rw-r--r--CHANGELOG.adoc10
-rw-r--r--lib/paseto/paseto.go330
-rw-r--r--lib/paseto/paseto_test.go269
-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.go305
-rw-r--r--lib/paseto/v2/paseto_test.go264
-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 {