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