aboutsummaryrefslogtreecommitdiff
path: root/lib/paseto/v4/public_mode.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/paseto/v4/public_mode.go')
-rw-r--r--lib/paseto/v4/public_mode.go112
1 files changed, 112 insertions, 0 deletions
diff --git a/lib/paseto/v4/public_mode.go b/lib/paseto/v4/public_mode.go
new file mode 100644
index 00000000..3ea901eb
--- /dev/null
+++ b/lib/paseto/v4/public_mode.go
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2026 M. Shulhan <ms@kilabit.info>
+
+package pasetov4
+
+import (
+ "bytes"
+ "crypto/ed25519"
+ "encoding/base64"
+ "fmt"
+ "slices"
+ "strings"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/paseto"
+)
+
+const publicHeader = `v4.public.`
+
+// PublicMode contains ed25519 private and public key for signing and
+// verifying message.
+type PublicMode struct {
+ priv ed25519.PrivateKey
+ pub ed25519.PublicKey
+}
+
+// NewPublicMode returns new instance of public mode from ed25519 seeds.
+func NewPublicMode(seed []byte) (pmode *PublicMode) {
+ pmode = &PublicMode{}
+ pmode.priv = ed25519.NewKeyFromSeed(seed)
+ pmode.pub = pmode.priv.Public().(ed25519.PublicKey)
+ return pmode
+}
+
+// Sign returns the public token that has been signing with private key.
+// The token contains msg and optional footer.
+func (pmode *PublicMode) Sign(msg, footer, implicit []byte) (token string, err error) {
+ logp := `Sign`
+
+ // Step 3: pack header, message, footer, and implicit.
+ pae, err := paseto.PreAuthEncode([]byte(publicHeader), msg, footer, implicit)
+ if err != nil {
+ return ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ // Step 4: Sign pae.
+ sig := ed25519.Sign(pmode.priv, pae)
+
+ // Step 5: Pack all into token,
+ var buf bytes.Buffer
+
+ buf.WriteString(publicHeader)
+
+ payload := slices.Concat(msg, sig)
+ n := base64.RawURLEncoding.EncodedLen(len(payload))
+ b64 := make([]byte, n)
+ base64.RawURLEncoding.Encode(b64, payload)
+ buf.Write(b64)
+
+ if len(footer) != 0 {
+ buf.WriteByte('.')
+ n = base64.RawURLEncoding.EncodedLen(len(footer))
+ b64 = make([]byte, n)
+ base64.RawURLEncoding.Encode(b64, footer)
+ buf.Write(b64)
+ }
+ return buf.String(), nil
+}
+
+// Verify returns the msg, with optional footer data, inside the token only if
+// its signature is valid.
+func (pmode *PublicMode) Verify(token string, implicit []byte) (msg, footer []byte, err error) {
+ logp := `Verify`
+
+ // Step 3: verify the header and unpack the footer if it exists.
+ token, found := strings.CutPrefix(token, publicHeader)
+ if !found {
+ return nil, nil, fmt.Errorf(`%s: invalid header, want %s`, logp, publicHeader)
+ }
+ token, footerb64, found := strings.Cut(token, `.`)
+ if found {
+ footer, err = base64.RawURLEncoding.DecodeString(footerb64)
+ if err != nil {
+ return nil, nil, fmt.Errorf(`%s: invalid footer: %w`, logp, err)
+ }
+ }
+
+ // Step 4: Decodes the payload.
+ payload, err := base64.RawURLEncoding.DecodeString(token)
+ if err != nil {
+ return nil, nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ lenpayload := len(payload)
+ if lenpayload <= ed25519.SignatureSize {
+ return nil, nil, fmt.Errorf(`%s: invalid payload size, want %d got %d`,
+ logp, ed25519.SignatureSize, len(payload))
+ }
+ msg = payload[:lenpayload-64]
+ sig := payload[lenpayload-64:]
+
+ // Step 5: Generate PAE.
+ pae, err := paseto.PreAuthEncode([]byte(publicHeader), msg, footer, implicit)
+ if err != nil {
+ return nil, nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ // Step 6: Verify the signature.
+ if !ed25519.Verify(pmode.pub, pae, sig) {
+ return nil, nil, fmt.Errorf(`%s: invalid message signature`, logp)
+ }
+
+ return msg, footer, nil
+}