diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /services/mailer/token/token.go | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | services/mailer/token/token.go | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go new file mode 100644 index 0000000..1a52bce --- /dev/null +++ b/services/mailer/token/token.go @@ -0,0 +1,138 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package token + +import ( + "context" + crypto_hmac "crypto/hmac" + "crypto/sha256" + "encoding/base32" + "fmt" + "time" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" +) + +// A token is a verifiable container describing an action. +// +// A token has a dynamic length depending on the contained data and has the following structure: +// | Token Version | User ID | HMAC | Payload | +// +// The payload is verifiable by the generated HMAC using the user secret. It contains: +// | Timestamp | Action/Handler Type | Action/Handler Data | +// +// +// Version changelog +// +// v1 -> v2: +// Use 128 instead of 80 bits of the HMAC-SHA256 output. + +const ( + tokenVersion1 byte = 1 + tokenVersion2 byte = 2 + tokenLifetimeInYears int = 1 +) + +type HandlerType byte + +const ( + UnknownHandlerType HandlerType = iota + ReplyHandlerType + UnsubscribeHandlerType +) + +var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding) + +type ErrToken struct { + context string +} + +func (err *ErrToken) Error() string { + return "invalid email token: " + err.context +} + +func (err *ErrToken) Unwrap() error { + return util.ErrInvalidArgument +} + +// CreateToken creates a token for the action/user tuple +func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, error) { + payload, err := util.PackData( + time.Now().AddDate(tokenLifetimeInYears, 0, 0).Unix(), + ht, + data, + ) + if err != nil { + return "", err + } + + packagedData, err := util.PackData( + user.ID, + generateHmac([]byte(user.Rands), payload), + payload, + ) + if err != nil { + return "", err + } + + return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion2}, packagedData...)), nil +} + +// ExtractToken extracts the action/user tuple from the token and verifies the content +func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) { + data, err := encodingWithoutPadding.DecodeString(token) + if err != nil { + return UnknownHandlerType, nil, nil, err + } + + if len(data) < 1 { + return UnknownHandlerType, nil, nil, &ErrToken{"no data"} + } + + if data[0] != tokenVersion2 { + return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])} + } + + var userID int64 + var hmac []byte + var payload []byte + if err := util.UnpackData(data[1:], &userID, &hmac, &payload); err != nil { + return UnknownHandlerType, nil, nil, err + } + + user, err := user_model.GetUserByID(ctx, userID) + if err != nil { + return UnknownHandlerType, nil, nil, err + } + + if !crypto_hmac.Equal(hmac, generateHmac([]byte(user.Rands), payload)) { + return UnknownHandlerType, nil, nil, &ErrToken{"verification failed"} + } + + var expiresUnix int64 + var handlerType HandlerType + var innerPayload []byte + if err := util.UnpackData(payload, &expiresUnix, &handlerType, &innerPayload); err != nil { + return UnknownHandlerType, nil, nil, err + } + + if time.Unix(expiresUnix, 0).Before(time.Now()) { + return UnknownHandlerType, nil, nil, &ErrToken{"token expired"} + } + + return handlerType, user, innerPayload, nil +} + +// generateHmac creates a trunkated HMAC for the given payload +func generateHmac(secret, payload []byte) []byte { + mac := crypto_hmac.New(sha256.New, secret) + mac.Write(payload) + hmac := mac.Sum(nil) + + // RFC2104 section 5 recommends that if you do HMAC truncation, you should use + // the max(80, hash_len/2) of the leftmost bits. + // For SHA256 this works out to using 128 of the leftmost bits. + return hmac[:16] +} |