mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-12-01 06:16:09 +01:00
9508aa7713
- If the incoming mail feature is enabled, tokens are being sent with outgoing mails. These tokens contains information about what type of action is allow with such token (such as replying to a certain issue ID), to verify these tokens the code uses the HMAC-SHA256 construction. - The output of the HMAC is truncated to 80 bits, because this is recommended by RFC2104, but RFC2104 actually doesn't recommend this. It recommends, if truncation should need to take place, it should use max(80, hash_len/2) of the leftmost bits. For HMAC-SHA256 this works out to 128 bits instead of the currently used 80 bits. - Update to token version 2 and disallow any usage of token version 1, token version 2 are generated with 128 bits of HMAC output. - Add test to verify the deprecation of token version 1 and a general MAC check test.
139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
// 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]
|
|
}
|