code.gitea.io/gitea@v1.21.7/services/mailer/token/token.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package token
     5  
     6  import (
     7  	"context"
     8  	crypto_hmac "crypto/hmac"
     9  	"encoding/base32"
    10  	"fmt"
    11  	"time"
    12  
    13  	user_model "code.gitea.io/gitea/models/user"
    14  	"code.gitea.io/gitea/modules/util"
    15  
    16  	"github.com/minio/sha256-simd"
    17  )
    18  
    19  // A token is a verifiable container describing an action.
    20  //
    21  // A token has a dynamic length depending on the contained data and has the following structure:
    22  // | Token Version | User ID | HMAC | Payload |
    23  //
    24  // The payload is verifiable by the generated HMAC using the user secret. It contains:
    25  // | Timestamp | Action/Handler Type | Action/Handler Data |
    26  
    27  const (
    28  	tokenVersion1        byte = 1
    29  	tokenLifetimeInYears int  = 1
    30  )
    31  
    32  type HandlerType byte
    33  
    34  const (
    35  	UnknownHandlerType HandlerType = iota
    36  	ReplyHandlerType
    37  	UnsubscribeHandlerType
    38  )
    39  
    40  var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
    41  
    42  type ErrToken struct {
    43  	context string
    44  }
    45  
    46  func (err *ErrToken) Error() string {
    47  	return "invalid email token: " + err.context
    48  }
    49  
    50  func (err *ErrToken) Unwrap() error {
    51  	return util.ErrInvalidArgument
    52  }
    53  
    54  // CreateToken creates a token for the action/user tuple
    55  func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, error) {
    56  	payload, err := util.PackData(
    57  		time.Now().AddDate(tokenLifetimeInYears, 0, 0).Unix(),
    58  		ht,
    59  		data,
    60  	)
    61  	if err != nil {
    62  		return "", err
    63  	}
    64  
    65  	packagedData, err := util.PackData(
    66  		user.ID,
    67  		generateHmac([]byte(user.Rands), payload),
    68  		payload,
    69  	)
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  
    74  	return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
    75  }
    76  
    77  // ExtractToken extracts the action/user tuple from the token and verifies the content
    78  func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
    79  	data, err := encodingWithoutPadding.DecodeString(token)
    80  	if err != nil {
    81  		return UnknownHandlerType, nil, nil, err
    82  	}
    83  
    84  	if len(data) < 1 {
    85  		return UnknownHandlerType, nil, nil, &ErrToken{"no data"}
    86  	}
    87  
    88  	if data[0] != tokenVersion1 {
    89  		return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])}
    90  	}
    91  
    92  	var userID int64
    93  	var hmac []byte
    94  	var payload []byte
    95  	if err := util.UnpackData(data[1:], &userID, &hmac, &payload); err != nil {
    96  		return UnknownHandlerType, nil, nil, err
    97  	}
    98  
    99  	user, err := user_model.GetUserByID(ctx, userID)
   100  	if err != nil {
   101  		return UnknownHandlerType, nil, nil, err
   102  	}
   103  
   104  	if !crypto_hmac.Equal(hmac, generateHmac([]byte(user.Rands), payload)) {
   105  		return UnknownHandlerType, nil, nil, &ErrToken{"verification failed"}
   106  	}
   107  
   108  	var expiresUnix int64
   109  	var handlerType HandlerType
   110  	var innerPayload []byte
   111  	if err := util.UnpackData(payload, &expiresUnix, &handlerType, &innerPayload); err != nil {
   112  		return UnknownHandlerType, nil, nil, err
   113  	}
   114  
   115  	if time.Unix(expiresUnix, 0).Before(time.Now()) {
   116  		return UnknownHandlerType, nil, nil, &ErrToken{"token expired"}
   117  	}
   118  
   119  	return handlerType, user, innerPayload, nil
   120  }
   121  
   122  // generateHmac creates a trunkated HMAC for the given payload
   123  func generateHmac(secret, payload []byte) []byte {
   124  	mac := crypto_hmac.New(sha256.New, secret)
   125  	mac.Write(payload)
   126  	hmac := mac.Sum(nil)
   127  
   128  	return hmac[:10] // RFC2104 recommends not using less then 80 bits
   129  }