mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
2978 files. The fork is 2026-07-15 (rebrand: gitea.dev -> github.com/hanzoai/git), so 2026 is the year, and "Copyright 2026 Hanzo AI, Inc." was already the form used by the handful of files that had ours. Shape is now: // Copyright 2026 Hanzo AI, Inc. All rights reserved. // Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT Ours on top, theirs retained verbatim. Retained because MIT requires it, and separately because they are true — those authors did write that code. Not renamed to "The Git Authors": Git is a different project under a different license, and putting its name here would attribute this code to people who have nothing to do with it. Adding our line is the fix; overwriting theirs would have been both a licence violation and a false statement. gofmt still reports import-order drift in some of these files. It predates this change (verified against a stash) and is left alone rather than burying a one-line-per-file legal change under 3000 files of unrelated reordering. Build clean. Co-authored-by: Hanzo Dev <[email protected]>
133 lines
3.5 KiB
Go
133 lines
3.5 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// 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"
|
|
"strings"
|
|
"time"
|
|
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/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 |
|
|
|
|
const (
|
|
tokenVersion1 byte = 1
|
|
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{tokenVersion1}, packagedData...)), nil
|
|
}
|
|
|
|
// DecodeToken decodes the handler, user and payload from the token and verifies the content
|
|
func DecodeToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
|
|
// MTAs are permitted to alter the case of the local-part (RFC 5321 §2.4), so normalize
|
|
// to the base32 alphabet before decoding to survive a lowercased reply-to address.
|
|
data, err := encodingWithoutPadding.DecodeString(strings.ToUpper(token))
|
|
if err != nil {
|
|
return UnknownHandlerType, nil, nil, err
|
|
}
|
|
|
|
if len(data) < 1 {
|
|
return UnknownHandlerType, nil, nil, &ErrToken{"no data"}
|
|
}
|
|
|
|
if data[0] != tokenVersion1 {
|
|
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 truncated 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)
|
|
|
|
return hmac[:10] // RFC2104 recommends not using less than 80 bits
|
|
}
|