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 }