pkg/sdk/security/crypto/paseto/v4/helpers.go (216 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package v4
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/elastic/harp/pkg/sdk/security"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/chacha20"
)
const (
// KeyLength is the requested encryption key size.
KeyLength = 32
nonceLength = 32
macLength = 32
encryptionKDFLength = 56
authenticationKeyLength = 32
v4LocalPrefix = "v4.local."
v4PublicPrefix = "v4.public."
)
// PASETO v4 symmetric encryption primitive.
// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#encrypt
func Encrypt(r io.Reader, key, m []byte, f, i string) ([]byte, error) {
// Create random seed
var n [nonceLength]byte
if _, err := io.ReadFull(r, n[:]); err != nil {
return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err)
}
// Delegate to primitive
return encrypt(key, n[:], m, f, i)
}
// PASETO v4 symmetric decryption primitive
// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#decrypt
func Decrypt(key, input []byte, f, i string) ([]byte, error) {
// Check arguments
if key == nil {
return nil, errors.New("paseto: key is nil")
}
if len(key) != KeyLength {
return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength)
}
if input == nil {
return nil, errors.New("paseto: input is nil")
}
// Check token header
if !bytes.HasPrefix(input, []byte(v4LocalPrefix)) {
return nil, errors.New("paseto: invalid token")
}
// Trim prefix
input = input[len(v4LocalPrefix):]
// Check footer usage
if f != "" {
// Split the footer and the body
parts := bytes.SplitN(input, []byte("."), 2)
if len(parts) != 2 {
return nil, errors.New("paseto: invalid token, footer is missing but expected")
}
// Decode footer
footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1])))
if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil {
return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err)
}
// Compare footer
if !security.SecureCompare([]byte(f), footer) {
return nil, errors.New("paseto: invalid token, footer mismatch")
}
// Continue without footer
input = parts[0]
}
// Decode token
raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(input)))
if _, err := base64.RawURLEncoding.Decode(raw, input); err != nil {
return nil, fmt.Errorf("paseto: invalid token body: %w", err)
}
// Extract components
n := raw[:nonceLength]
t := raw[len(raw)-macLength:]
c := raw[macLength : len(raw)-macLength]
// Derive keys from seed and secret key
ek, n2, ak, err := kdf(key, n)
if err != nil {
return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err)
}
// Compute MAC
t2, err := mac(ak, v4LocalPrefix, n, c, f, i)
if err != nil {
return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err)
}
// Time-constant compare MAC
if !security.SecureCompare(t, t2) {
return nil, errors.New("paseto: invalid pre-authentication header")
}
// Prepare XChaCha20 stream cipher
ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2)
if err != nil {
return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err)
}
// Encrypt the payload
m := make([]byte, len(c))
ciph.XORKeyStream(m, c)
// No error
return m, nil
}
// PASETO v4 public signature primitive.
// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#sign
func Sign(m []byte, sk ed25519.PrivateKey, f, i string) ([]byte, error) {
// Compute protected content
m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i))
if err != nil {
return nil, fmt.Errorf("unable to prepare protected content: %w", err)
}
// Sign protected content
sig := ed25519.Sign(sk, m2)
// Prepare content
body := append([]byte{}, m...)
body = append(body, sig...)
// Encode body as RawURLBase64
encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body)))
base64.RawURLEncoding.Encode(encodedBody, body)
// Assemble final token
final := append([]byte(v4PublicPrefix), encodedBody...)
if f != "" {
// Encode footer as RawURLBase64
encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f)))
base64.RawURLEncoding.Encode(encodedFooter, []byte(f))
// Assemble body and footer
final = append(final, append([]byte("."), encodedFooter...)...)
}
// No error
return final, nil
}
// PASETO v4 signature verification primitive.
// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#verify
func Verify(sm []byte, pk ed25519.PublicKey, f, i string) ([]byte, error) {
// Check token header
if !bytes.HasPrefix(sm, []byte(v4PublicPrefix)) {
return nil, errors.New("paseto: invalid token")
}
// Trim prefix
sm = sm[len(v4PublicPrefix):]
// Check footer usage
if f != "" {
// Split the footer and the body
parts := bytes.SplitN(sm, []byte("."), 2)
if len(parts) != 2 {
return nil, errors.New("paseto: invalid token, footer is missing but expected")
}
// Decode footer
footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1])))
if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil {
return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err)
}
// Compare footer
if !security.SecureCompare([]byte(f), footer) {
return nil, errors.New("paseto: invalid token, footer mismatch")
}
// Continue without footer
sm = parts[0]
}
// Decode token
raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(sm)))
if _, err := base64.RawURLEncoding.Decode(raw, sm); err != nil {
return nil, fmt.Errorf("paseto: invalid token body: %w", err)
}
// Extract components
m := raw[:len(raw)-ed25519.SignatureSize]
s := raw[len(raw)-ed25519.SignatureSize:]
// Compute protected content
m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i))
if err != nil {
return nil, fmt.Errorf("unable to prepare protected content: %w", err)
}
// Check signature
if !ed25519.Verify(pk, m2, s) {
return nil, errors.New("paseto: invalid token signature")
}
// No error
return m, nil
}
// -----------------------------------------------------------------------------
func encrypt(key, n, m []byte, f, i string) ([]byte, error) {
// Check arguments
if len(key) != KeyLength {
return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength)
}
if len(n) != nonceLength {
return nil, fmt.Errorf("paseto: invalid nonce length, it must be %d bytes long", nonceLength)
}
// Derive keys from seed and secret key
ek, n2, ak, err := kdf(key, n)
if err != nil {
return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err)
}
// Prepare XChaCha20 stream cipher (nonce > 24bytes => XChacha)
ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2)
if err != nil {
return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err)
}
// Encrypt the payload
c := make([]byte, len(m))
ciph.XORKeyStream(c, m)
// Compute MAC
t, err := mac(ak, v4LocalPrefix, n, c, f, i)
if err != nil {
return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err)
}
// Serialize final token
// h || base64url(n || c || t)
body := append([]byte{}, n...)
body = append(body, c...)
body = append(body, t...)
// Encode body as RawURLBase64
encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body)))
base64.RawURLEncoding.Encode(encodedBody, body)
// Assemble final token
final := append([]byte(v4LocalPrefix), encodedBody...)
if f != "" {
// Encode footer as RawURLBase64
encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f)))
base64.RawURLEncoding.Encode(encodedFooter, []byte(f))
// Assemble body and footer
final = append(final, append([]byte("."), encodedFooter...)...)
}
// No error
return final, nil
}
func kdf(key, n []byte) (ek, n2, ak []byte, err error) {
// Derive encryption key
encKDF, err := blake2b.New(encryptionKDFLength, key)
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to initialize encryption kdf: %w", err)
}
// Domain separation (we use the same seed for 2 different purposes)
encKDF.Write([]byte("paseto-encryption-key"))
encKDF.Write(n)
tmp := encKDF.Sum(nil)
// Split encryption key (Ek) and nonce (n2)
ek = tmp[:KeyLength]
n2 = tmp[KeyLength:]
// Derive authentication key
authKDF, err := blake2b.New(authenticationKeyLength, key)
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to initialize authentication kdf: %w", err)
}
// Domain separation (we use the same seed for 2 different purposes)
authKDF.Write([]byte("paseto-auth-key-for-aead"))
authKDF.Write(n)
ak = authKDF.Sum(nil)
// No error
return ek, n2, ak, nil
}
func mac(ak []byte, h string, n, c []byte, f, i string) ([]byte, error) {
// Compute pre-authentication message
preAuth, err := pae([]byte(h), n, c, []byte(f), []byte(i))
if err != nil {
return nil, fmt.Errorf("unable to compute pre-authentication content: %w", err)
}
// Compute MAC
mac, err := blake2b.New(macLength, ak)
if err != nil {
return nil, fmt.Errorf("unable to in initialize MAC kdf: %w", err)
}
// Hash pre-authentication content
mac.Write(preAuth)
// No error
return mac.Sum(nil), nil
}
// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding
func pae(pieces ...[]byte) ([]byte, error) {
output := &bytes.Buffer{}
// Encode piece count
count := len(pieces)
if err := binary.Write(output, binary.LittleEndian, uint64(count)); err != nil {
return nil, err
}
// For each element
for i := range pieces {
// Encode size
if err := binary.Write(output, binary.LittleEndian, uint64(len(pieces[i]))); err != nil {
return nil, err
}
// Encode data
if _, err := output.Write(pieces[i]); err != nil {
return nil, err
}
}
// No error
return output.Bytes(), nil
}