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 }