aboutsummaryrefslogtreecommitdiff
path: root/lib/paseto/paseto.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/paseto/paseto.go')
-rw-r--r--lib/paseto/paseto.go330
1 files changed, 16 insertions, 314 deletions
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