diff options
Diffstat (limited to 'lib/paseto/v2/paseto.go')
| -rw-r--r-- | lib/paseto/v2/paseto.go | 305 |
1 files changed, 305 insertions, 0 deletions
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 +} |
