summaryrefslogtreecommitdiffstats
path: root/services/mailer/token/token.go
blob: 1a52bce803dc14f44c1501c1240361a8c5207b65 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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]
}