github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/security/crypto/paseto/v4/helpers.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package v4
    19  
    20  import (
    21  	"bytes"
    22  	"crypto/ed25519"
    23  	"encoding/base64"
    24  	"encoding/binary"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  
    29  	"github.com/zntrio/harp/v2/pkg/sdk/security"
    30  
    31  	"golang.org/x/crypto/blake2b"
    32  	"golang.org/x/crypto/chacha20"
    33  )
    34  
    35  const (
    36  	// KeyLength is the requested encryption key size.
    37  	KeyLength               = 32
    38  	nonceLength             = 32
    39  	macLength               = 32
    40  	encryptionKDFLength     = 56
    41  	authenticationKeyLength = 32
    42  	v4LocalPrefix           = "v4.local."
    43  	v4PublicPrefix          = "v4.public."
    44  )
    45  
    46  // PASETO v4 symmetric encryption primitive.
    47  // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#encrypt
    48  func Encrypt(r io.Reader, key, m []byte, f, i string) ([]byte, error) {
    49  	// Create random seed
    50  	var n [nonceLength]byte
    51  	if _, err := io.ReadFull(r, n[:]); err != nil {
    52  		return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err)
    53  	}
    54  
    55  	// Delegate to primitive
    56  	return encrypt(key, n[:], m, f, i)
    57  }
    58  
    59  // PASETO v4 symmetric decryption primitive
    60  // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#decrypt
    61  func Decrypt(key, input []byte, f, i string) ([]byte, error) {
    62  	// Check arguments
    63  	if key == nil {
    64  		return nil, errors.New("paseto: key is nil")
    65  	}
    66  	if len(key) != KeyLength {
    67  		return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength)
    68  	}
    69  	if input == nil {
    70  		return nil, errors.New("paseto: input is nil")
    71  	}
    72  
    73  	// Check token header
    74  	if !bytes.HasPrefix(input, []byte(v4LocalPrefix)) {
    75  		return nil, errors.New("paseto: invalid token")
    76  	}
    77  
    78  	// Trim prefix
    79  	input = input[len(v4LocalPrefix):]
    80  
    81  	// Check footer usage
    82  	if f != "" {
    83  		// Split the footer and the body
    84  		parts := bytes.SplitN(input, []byte("."), 2)
    85  		if len(parts) != 2 {
    86  			return nil, errors.New("paseto: invalid token, footer is missing but expected")
    87  		}
    88  
    89  		// Decode footer
    90  		footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1])))
    91  		if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil {
    92  			return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err)
    93  		}
    94  
    95  		// Compare footer
    96  		if !security.SecureCompare([]byte(f), footer) {
    97  			return nil, errors.New("paseto: invalid token, footer mismatch")
    98  		}
    99  
   100  		// Continue without footer
   101  		input = parts[0]
   102  	}
   103  
   104  	// Decode token
   105  	raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(input)))
   106  	if _, err := base64.RawURLEncoding.Decode(raw, input); err != nil {
   107  		return nil, fmt.Errorf("paseto: invalid token body: %w", err)
   108  	}
   109  
   110  	// Extract components
   111  	n := raw[:nonceLength]
   112  	t := raw[len(raw)-macLength:]
   113  	c := raw[macLength : len(raw)-macLength]
   114  
   115  	// Derive keys from seed and secret key
   116  	ek, n2, ak, err := kdf(key, n)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err)
   119  	}
   120  
   121  	// Compute MAC
   122  	t2, err := mac(ak, v4LocalPrefix, n, c, f, i)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err)
   125  	}
   126  
   127  	// Time-constant compare MAC
   128  	if !security.SecureCompare(t, t2) {
   129  		return nil, errors.New("paseto: invalid pre-authentication header")
   130  	}
   131  
   132  	// Prepare XChaCha20 stream cipher
   133  	ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2)
   134  	if err != nil {
   135  		return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err)
   136  	}
   137  
   138  	// Encrypt the payload
   139  	m := make([]byte, len(c))
   140  	ciph.XORKeyStream(m, c)
   141  
   142  	// No error
   143  	return m, nil
   144  }
   145  
   146  // PASETO v4 public signature primitive.
   147  // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#sign
   148  func Sign(m []byte, sk ed25519.PrivateKey, f, i string) ([]byte, error) {
   149  	// Compute protected content
   150  	m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i))
   151  	if err != nil {
   152  		return nil, fmt.Errorf("unable to prepare protected content: %w", err)
   153  	}
   154  
   155  	// Sign protected content
   156  	sig := ed25519.Sign(sk, m2)
   157  
   158  	// Prepare content
   159  	body := append([]byte{}, m...)
   160  	body = append(body, sig...)
   161  
   162  	// Encode body as RawURLBase64
   163  	encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body)))
   164  	base64.RawURLEncoding.Encode(encodedBody, body)
   165  
   166  	// Assemble final token
   167  	final := append([]byte(v4PublicPrefix), encodedBody...)
   168  	if f != "" {
   169  		// Encode footer as RawURLBase64
   170  		encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f)))
   171  		base64.RawURLEncoding.Encode(encodedFooter, []byte(f))
   172  
   173  		// Assemble body and footer
   174  		final = append(final, append([]byte("."), encodedFooter...)...)
   175  	}
   176  
   177  	// No error
   178  	return final, nil
   179  }
   180  
   181  // PASETO v4 signature verification primitive.
   182  // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#verify
   183  func Verify(sm []byte, pk ed25519.PublicKey, f, i string) ([]byte, error) {
   184  	// Check token header
   185  	if !bytes.HasPrefix(sm, []byte(v4PublicPrefix)) {
   186  		return nil, errors.New("paseto: invalid token")
   187  	}
   188  
   189  	// Trim prefix
   190  	sm = sm[len(v4PublicPrefix):]
   191  
   192  	// Check footer usage
   193  	if f != "" {
   194  		// Split the footer and the body
   195  		parts := bytes.SplitN(sm, []byte("."), 2)
   196  		if len(parts) != 2 {
   197  			return nil, errors.New("paseto: invalid token, footer is missing but expected")
   198  		}
   199  
   200  		// Decode footer
   201  		footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1])))
   202  		if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil {
   203  			return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err)
   204  		}
   205  
   206  		// Compare footer
   207  		if !security.SecureCompare([]byte(f), footer) {
   208  			return nil, errors.New("paseto: invalid token, footer mismatch")
   209  		}
   210  
   211  		// Continue without footer
   212  		sm = parts[0]
   213  	}
   214  
   215  	// Decode token
   216  	raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(sm)))
   217  	if _, err := base64.RawURLEncoding.Decode(raw, sm); err != nil {
   218  		return nil, fmt.Errorf("paseto: invalid token body: %w", err)
   219  	}
   220  
   221  	// Extract components
   222  	m := raw[:len(raw)-ed25519.SignatureSize]
   223  	s := raw[len(raw)-ed25519.SignatureSize:]
   224  
   225  	// Compute protected content
   226  	m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i))
   227  	if err != nil {
   228  		return nil, fmt.Errorf("unable to prepare protected content: %w", err)
   229  	}
   230  
   231  	// Check signature
   232  	if !ed25519.Verify(pk, m2, s) {
   233  		return nil, errors.New("paseto: invalid token signature")
   234  	}
   235  
   236  	// No error
   237  	return m, nil
   238  }
   239  
   240  // -----------------------------------------------------------------------------
   241  
   242  func encrypt(key, n, m []byte, f, i string) ([]byte, error) {
   243  	// Check arguments
   244  	if len(key) != KeyLength {
   245  		return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength)
   246  	}
   247  	if len(n) != nonceLength {
   248  		return nil, fmt.Errorf("paseto: invalid nonce length, it must be %d bytes long", nonceLength)
   249  	}
   250  
   251  	// Derive keys from seed and secret key
   252  	ek, n2, ak, err := kdf(key, n)
   253  	if err != nil {
   254  		return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err)
   255  	}
   256  
   257  	// Prepare XChaCha20 stream cipher (nonce > 24bytes => XChacha)
   258  	ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2)
   259  	if err != nil {
   260  		return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err)
   261  	}
   262  
   263  	// Encrypt the payload
   264  	c := make([]byte, len(m))
   265  	ciph.XORKeyStream(c, m)
   266  
   267  	// Compute MAC
   268  	t, err := mac(ak, v4LocalPrefix, n, c, f, i)
   269  	if err != nil {
   270  		return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err)
   271  	}
   272  
   273  	// Serialize final token
   274  	// h || base64url(n || c || t)
   275  	body := append([]byte{}, n...)
   276  	body = append(body, c...)
   277  	body = append(body, t...)
   278  
   279  	// Encode body as RawURLBase64
   280  	encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body)))
   281  	base64.RawURLEncoding.Encode(encodedBody, body)
   282  
   283  	// Assemble final token
   284  	final := append([]byte(v4LocalPrefix), encodedBody...)
   285  	if f != "" {
   286  		// Encode footer as RawURLBase64
   287  		encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f)))
   288  		base64.RawURLEncoding.Encode(encodedFooter, []byte(f))
   289  
   290  		// Assemble body and footer
   291  		final = append(final, append([]byte("."), encodedFooter...)...)
   292  	}
   293  
   294  	// No error
   295  	return final, nil
   296  }
   297  
   298  func kdf(key, n []byte) (ek, n2, ak []byte, err error) {
   299  	// Derive encryption key
   300  	encKDF, err := blake2b.New(encryptionKDFLength, key)
   301  	if err != nil {
   302  		return nil, nil, nil, fmt.Errorf("unable to initialize encryption kdf: %w", err)
   303  	}
   304  
   305  	// Domain separation (we use the same seed for 2 different purposes)
   306  	encKDF.Write([]byte("paseto-encryption-key"))
   307  	encKDF.Write(n)
   308  	tmp := encKDF.Sum(nil)
   309  
   310  	// Split encryption key (Ek) and nonce (n2)
   311  	ek = tmp[:KeyLength]
   312  	n2 = tmp[KeyLength:]
   313  
   314  	// Derive authentication key
   315  	authKDF, err := blake2b.New(authenticationKeyLength, key)
   316  	if err != nil {
   317  		return nil, nil, nil, fmt.Errorf("unable to initialize authentication kdf: %w", err)
   318  	}
   319  
   320  	// Domain separation (we use the same seed for 2 different purposes)
   321  	authKDF.Write([]byte("paseto-auth-key-for-aead"))
   322  	authKDF.Write(n)
   323  	ak = authKDF.Sum(nil)
   324  
   325  	// No error
   326  	return ek, n2, ak, nil
   327  }
   328  
   329  func mac(ak []byte, h string, n, c []byte, f, i string) ([]byte, error) {
   330  	// Compute pre-authentication message
   331  	preAuth, err := pae([]byte(h), n, c, []byte(f), []byte(i))
   332  	if err != nil {
   333  		return nil, fmt.Errorf("unable to compute pre-authentication content: %w", err)
   334  	}
   335  
   336  	// Compute MAC
   337  	mac, err := blake2b.New(macLength, ak)
   338  	if err != nil {
   339  		return nil, fmt.Errorf("unable to in initialize MAC kdf: %w", err)
   340  	}
   341  
   342  	// Hash pre-authentication content
   343  	mac.Write(preAuth)
   344  
   345  	// No error
   346  	return mac.Sum(nil), nil
   347  }
   348  
   349  // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding
   350  func pae(pieces ...[]byte) ([]byte, error) {
   351  	output := &bytes.Buffer{}
   352  
   353  	// Encode piece count
   354  	count := len(pieces)
   355  	if err := binary.Write(output, binary.LittleEndian, uint64(count)); err != nil {
   356  		return nil, err
   357  	}
   358  
   359  	// For each element
   360  	for i := range pieces {
   361  		// Encode size
   362  		if err := binary.Write(output, binary.LittleEndian, uint64(len(pieces[i]))); err != nil {
   363  			return nil, err
   364  		}
   365  
   366  		// Encode data
   367  		if _, err := output.Write(pieces[i]); err != nil {
   368  			return nil, err
   369  		}
   370  	}
   371  
   372  	// No error
   373  	return output.Bytes(), nil
   374  }