Skip to content

Commit 55cb356

Browse files
GiteaBotwxiaoguang
andauthored
Refactor sha1 and time-limited code (#31023) (#31030)
Backport #31023 by wxiaoguang Co-authored-by: wxiaoguang <[email protected]>
1 parent 8a259e5 commit 55cb356

File tree

8 files changed

+122
-98
lines changed

8 files changed

+122
-98
lines changed

models/user/email_address.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/mail"
1111
"regexp"
1212
"strings"
13+
"time"
1314

1415
"code.gitea.io/gitea/models/db"
1516
"code.gitea.io/gitea/modules/base"
@@ -353,14 +354,12 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne
353354

354355
// VerifyActiveEmailCode verifies active email code when active account
355356
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
356-
minutes := setting.Service.ActiveCodeLives
357-
358357
if user := GetVerifyUser(ctx, code); user != nil {
359358
// time limit code
360359
prefix := code[:base.TimeLimitCodeLength]
361360
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
362361

363-
if base.VerifyTimeLimitCode(data, minutes, prefix) {
362+
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
364363
emailAddress := &EmailAddress{UID: user.ID, Email: email}
365364
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
366365
return emailAddress

models/user/user.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ func (u *User) OrganisationLink() string {
304304
func (u *User) GenerateEmailActivateCode(email string) string {
305305
code := base.CreateTimeLimitCode(
306306
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
307-
setting.Service.ActiveCodeLives, nil)
307+
setting.Service.ActiveCodeLives, time.Now(), nil)
308308

309309
// Add tail hex username
310310
code += hex.EncodeToString([]byte(u.LowerName))
@@ -791,14 +791,11 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) {
791791

792792
// VerifyUserActiveCode verifies active code when active account
793793
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
794-
minutes := setting.Service.ActiveCodeLives
795-
796794
if user = GetVerifyUser(ctx, code); user != nil {
797795
// time limit code
798796
prefix := code[:base.TimeLimitCodeLength]
799797
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
800-
801-
if base.VerifyTimeLimitCode(data, minutes, prefix) {
798+
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
802799
return user
803800
}
804801
}

modules/base/tool.go

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
package base
55

66
import (
7+
"crypto/hmac"
78
"crypto/sha1"
89
"crypto/sha256"
10+
"crypto/subtle"
911
"encoding/base64"
1012
"encoding/hex"
1113
"errors"
1214
"fmt"
15+
"hash"
1316
"os"
1417
"path/filepath"
1518
"runtime"
@@ -25,13 +28,6 @@ import (
2528
"github.com/dustin/go-humanize"
2629
)
2730

28-
// EncodeSha1 string to sha1 hex value.
29-
func EncodeSha1(str string) string {
30-
h := sha1.New()
31-
_, _ = h.Write([]byte(str))
32-
return hex.EncodeToString(h.Sum(nil))
33-
}
34-
3531
// EncodeSha256 string to sha256 hex value.
3632
func EncodeSha256(str string) string {
3733
h := sha256.New()
@@ -62,63 +58,62 @@ func BasicAuthDecode(encoded string) (string, string, error) {
6258
}
6359

6460
// VerifyTimeLimitCode verify time limit code
65-
func VerifyTimeLimitCode(data string, minutes int, code string) bool {
61+
func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool {
6662
if len(code) <= 18 {
6763
return false
6864
}
6965

70-
// split code
71-
start := code[:12]
72-
lives := code[12:18]
73-
if d, err := strconv.ParseInt(lives, 10, 0); err == nil {
74-
minutes = int(d)
75-
}
66+
startTimeStr := code[:12]
67+
aliveTimeStr := code[12:18]
68+
aliveTime, _ := strconv.Atoi(aliveTimeStr) // no need to check err, if anything wrong, the following code check will fail soon
7669

77-
// right active code
78-
retCode := CreateTimeLimitCode(data, minutes, start)
79-
if retCode == code && minutes > 0 {
80-
// check time is expired or not
81-
before, _ := time.ParseInLocation("200601021504", start, time.Local)
82-
now := time.Now()
83-
if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
84-
return true
70+
// check code
71+
retCode := CreateTimeLimitCode(data, aliveTime, startTimeStr, nil)
72+
if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
73+
retCode = CreateTimeLimitCode(data, aliveTime, startTimeStr, sha1.New()) // TODO: this is only for the support of legacy codes, remove this in/after 1.23
74+
if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
75+
return false
8576
}
8677
}
8778

88-
return false
79+
// check time is expired or not: startTime <= now && now < startTime + minutes
80+
startTime, _ := time.ParseInLocation("200601021504", startTimeStr, time.Local)
81+
return (startTime.Before(now) || startTime.Equal(now)) && now.Before(startTime.Add(time.Minute*time.Duration(minutes)))
8982
}
9083

9184
// TimeLimitCodeLength default value for time limit code
9285
const TimeLimitCodeLength = 12 + 6 + 40
9386

94-
// CreateTimeLimitCode create a time limit code
95-
// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
96-
func CreateTimeLimitCode(data string, minutes int, startInf any) string {
97-
format := "200601021504"
98-
99-
var start, end time.Time
100-
var startStr, endStr string
87+
// CreateTimeLimitCode create a time-limited code.
88+
// Format: 12 length date time string + 6 minutes string (not used) + 40 hash string, some other code depends on this fixed length
89+
// If h is nil, then use the default hmac hash.
90+
func CreateTimeLimitCode[T time.Time | string](data string, minutes int, startTimeGeneric T, h hash.Hash) string {
91+
const format = "200601021504"
10192

102-
if startInf == nil {
103-
// Use now time create code
104-
start = time.Now()
105-
startStr = start.Format(format)
93+
var start time.Time
94+
var startTimeAny any = startTimeGeneric
95+
if t, ok := startTimeAny.(time.Time); ok {
96+
start = t
10697
} else {
107-
// use start string create code
108-
startStr = startInf.(string)
109-
start, _ = time.ParseInLocation(format, startStr, time.Local)
110-
startStr = start.Format(format)
98+
var err error
99+
start, err = time.ParseInLocation(format, startTimeAny.(string), time.Local)
100+
if err != nil {
101+
return "" // return an invalid code because the "parse" failed
102+
}
111103
}
104+
startStr := start.Format(format)
105+
end := start.Add(time.Minute * time.Duration(minutes))
112106

113-
end = start.Add(time.Minute * time.Duration(minutes))
114-
endStr = end.Format(format)
115-
116-
// create sha1 encode string
117-
sh := sha1.New()
118-
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
119-
encoded := hex.EncodeToString(sh.Sum(nil))
107+
if h == nil {
108+
h = hmac.New(sha1.New, setting.GetGeneralTokenSigningSecret())
109+
}
110+
_, _ = fmt.Fprintf(h, "%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, end.Format(format), minutes)
111+
encoded := hex.EncodeToString(h.Sum(nil))
120112

121113
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
114+
if len(code) != TimeLimitCodeLength {
115+
panic("there is a hard requirement for the length of time-limited code") // it shouldn't happen
116+
}
122117
return code
123118
}
124119

modules/base/tool_test.go

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,18 @@
44
package base
55

66
import (
7+
"crypto/sha1"
8+
"fmt"
79
"os"
810
"testing"
911
"time"
1012

13+
"code.gitea.io/gitea/modules/setting"
14+
"code.gitea.io/gitea/modules/test"
15+
1116
"github.com/stretchr/testify/assert"
1217
)
1318

14-
func TestEncodeSha1(t *testing.T) {
15-
assert.Equal(t,
16-
"8843d7f92416211de9ebb963ff4ce28125932878",
17-
EncodeSha1("foobar"),
18-
)
19-
}
20-
2119
func TestEncodeSha256(t *testing.T) {
2220
assert.Equal(t,
2321
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2",
@@ -46,43 +44,54 @@ func TestBasicAuthDecode(t *testing.T) {
4644
}
4745

4846
func TestVerifyTimeLimitCode(t *testing.T) {
49-
tc := []struct {
50-
data string
51-
minutes int
52-
code string
53-
valid bool
54-
}{{
55-
data: "data",
56-
minutes: 2,
57-
code: testCreateTimeLimitCode(t, "data", 2),
58-
valid: true,
59-
}, {
60-
data: "abc123-ß",
61-
minutes: 1,
62-
code: testCreateTimeLimitCode(t, "abc123-ß", 1),
63-
valid: true,
64-
}, {
65-
data: "data",
66-
minutes: 2,
67-
code: "2021012723240000005928251dac409d2c33a6eb82c63410aaad569bed",
68-
valid: false,
69-
}}
70-
for _, test := range tc {
71-
actualValid := VerifyTimeLimitCode(test.data, test.minutes, test.code)
72-
assert.Equal(t, test.valid, actualValid, "data: '%s' code: '%s' should be valid: %t", test.data, test.code, test.valid)
47+
defer test.MockVariableValue(&setting.InstallLock, true)()
48+
initGeneralSecret := func(secret string) {
49+
setting.InstallLock = true
50+
setting.CfgProvider, _ = setting.NewConfigProviderFromData(fmt.Sprintf(`
51+
[oauth2]
52+
JWT_SECRET = %s
53+
`, secret))
54+
setting.LoadCommonSettings()
7355
}
74-
}
75-
76-
func testCreateTimeLimitCode(t *testing.T, data string, m int) string {
77-
result0 := CreateTimeLimitCode(data, m, nil)
78-
result1 := CreateTimeLimitCode(data, m, time.Now().Format("200601021504"))
79-
result2 := CreateTimeLimitCode(data, m, time.Unix(time.Now().Unix()+int64(time.Minute)*int64(m), 0).Format("200601021504"))
80-
81-
assert.Equal(t, result0, result1)
82-
assert.NotEqual(t, result0, result2)
8356

84-
assert.True(t, len(result0) != 0)
85-
return result0
57+
initGeneralSecret("KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
58+
now := time.Now()
59+
60+
t.Run("TestGenericParameter", func(t *testing.T) {
61+
time2000 := time.Date(2000, 1, 2, 3, 4, 5, 0, time.Local)
62+
assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, time2000, sha1.New()))
63+
assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, "200001020304", sha1.New()))
64+
assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, time2000, nil))
65+
assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, "200001020304", nil))
66+
})
67+
68+
t.Run("TestInvalidCode", func(t *testing.T) {
69+
assert.False(t, VerifyTimeLimitCode(now, "data", 2, ""))
70+
assert.False(t, VerifyTimeLimitCode(now, "data", 2, "invalid code"))
71+
})
72+
73+
t.Run("TestCreateAndVerify", func(t *testing.T) {
74+
code := CreateTimeLimitCode("data", 2, now, nil)
75+
assert.False(t, VerifyTimeLimitCode(now.Add(-time.Minute), "data", 2, code)) // not started yet
76+
assert.True(t, VerifyTimeLimitCode(now, "data", 2, code))
77+
assert.True(t, VerifyTimeLimitCode(now.Add(time.Minute), "data", 2, code))
78+
assert.False(t, VerifyTimeLimitCode(now.Add(time.Minute), "DATA", 2, code)) // invalid data
79+
assert.False(t, VerifyTimeLimitCode(now.Add(2*time.Minute), "data", 2, code)) // expired
80+
})
81+
82+
t.Run("TestDifferentSecret", func(t *testing.T) {
83+
// use another secret to ensure the code is invalid for different secret
84+
verifyDataCode := func(c string) bool {
85+
return VerifyTimeLimitCode(now, "data", 2, c)
86+
}
87+
code1 := CreateTimeLimitCode("data", 2, now, sha1.New())
88+
code2 := CreateTimeLimitCode("data", 2, now, nil)
89+
assert.True(t, verifyDataCode(code1))
90+
assert.True(t, verifyDataCode(code2))
91+
initGeneralSecret("000_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
92+
assert.False(t, verifyDataCode(code1))
93+
assert.False(t, verifyDataCode(code2))
94+
})
8695
}
8796

8897
func TestFileSize(t *testing.T) {

modules/git/utils.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package git
55

66
import (
7+
"crypto/sha1"
8+
"encoding/hex"
79
"fmt"
810
"io"
911
"os"
@@ -128,3 +130,9 @@ func (l *LimitedReaderCloser) Read(p []byte) (n int, err error) {
128130
func (l *LimitedReaderCloser) Close() error {
129131
return l.C.Close()
130132
}
133+
134+
func HashFilePathForWebUI(s string) string {
135+
h := sha1.New()
136+
_, _ = h.Write([]byte(s))
137+
return hex.EncodeToString(h.Sum(nil))
138+
}

modules/git/utils_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package git
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestHashFilePathForWebUI(t *testing.T) {
13+
assert.Equal(t,
14+
"8843d7f92416211de9ebb963ff4ce28125932878",
15+
HashFilePathForWebUI("foobar"),
16+
)
17+
}

routers/web/repo/compare.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ func ExcerptBlob(ctx *context.Context) {
931931
}
932932
}
933933
ctx.Data["section"] = section
934-
ctx.Data["FileNameHash"] = base.EncodeSha1(filePath)
934+
ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath)
935935
ctx.Data["AfterCommitID"] = commitID
936936
ctx.Data["Anchor"] = anchor
937937
ctx.HTML(http.StatusOK, tplBlobExcerpt)

services/gitdiff/gitdiff.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
pull_model "code.gitea.io/gitea/models/pull"
2424
user_model "code.gitea.io/gitea/models/user"
2525
"code.gitea.io/gitea/modules/analyze"
26-
"code.gitea.io/gitea/modules/base"
2726
"code.gitea.io/gitea/modules/charset"
2827
"code.gitea.io/gitea/modules/git"
2928
"code.gitea.io/gitea/modules/highlight"
@@ -746,7 +745,7 @@ parsingLoop:
746745
diffLineTypeBuffers[DiffLineAdd] = new(bytes.Buffer)
747746
diffLineTypeBuffers[DiffLineDel] = new(bytes.Buffer)
748747
for _, f := range diff.Files {
749-
f.NameHash = base.EncodeSha1(f.Name)
748+
f.NameHash = git.HashFilePathForWebUI(f.Name)
750749

751750
for _, buffer := range diffLineTypeBuffers {
752751
buffer.Reset()

0 commit comments

Comments
 (0)