loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor sha1 and time-limited code (#31023)

Remove "EncodeSha1", it shouldn't be used as a general purpose hasher
(just like we have removed "EncodeMD5" in #28622)

Rewrite the "time-limited code" related code and write better tests, the
old code doesn't seem quite right.

(cherry picked from commit fb1ad920b769799aa1287441289d15477d9878c5)

Conflicts:
modules/git/utils_test.go
trivial context conflict because sha256 testing in Forgejo has diverged

authored by

wxiaoguang and committed by
Earl Warren
5612cf32 886a675f

+115 -97
+2 -3
models/user/email_address.go
··· 10 10 "net/mail" 11 11 "regexp" 12 12 "strings" 13 + "time" 13 14 14 15 "code.gitea.io/gitea/models/db" 15 16 "code.gitea.io/gitea/modules/base" ··· 362 363 363 364 // VerifyActiveEmailCode verifies active email code when active account 364 365 func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress { 365 - minutes := setting.Service.ActiveCodeLives 366 - 367 366 if user := GetVerifyUser(ctx, code); user != nil { 368 367 // time limit code 369 368 prefix := code[:base.TimeLimitCodeLength] 370 369 data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands) 371 370 372 - if base.VerifyTimeLimitCode(data, minutes, prefix) { 371 + if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { 373 372 emailAddress := &EmailAddress{UID: user.ID, Email: email} 374 373 if has, _ := db.GetEngine(ctx).Get(emailAddress); has { 375 374 return emailAddress
+2 -5
models/user/user.go
··· 321 321 func (u *User) GenerateEmailActivateCode(email string) string { 322 322 code := base.CreateTimeLimitCode( 323 323 fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands), 324 - setting.Service.ActiveCodeLives, nil) 324 + setting.Service.ActiveCodeLives, time.Now(), nil) 325 325 326 326 // Add tail hex username 327 327 code += hex.EncodeToString([]byte(u.LowerName)) ··· 818 818 819 819 // VerifyUserActiveCode verifies active code when active account 820 820 func VerifyUserActiveCode(ctx context.Context, code string) (user *User) { 821 - minutes := setting.Service.ActiveCodeLives 822 - 823 821 if user = GetVerifyUser(ctx, code); user != nil { 824 822 // time limit code 825 823 prefix := code[:base.TimeLimitCodeLength] 826 824 data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands) 827 - 828 - if base.VerifyTimeLimitCode(data, minutes, prefix) { 825 + if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { 829 826 return user 830 827 } 831 828 }
+40 -45
modules/base/tool.go
··· 4 4 package base 5 5 6 6 import ( 7 + "crypto/hmac" 7 8 "crypto/sha1" 8 9 "crypto/sha256" 10 + "crypto/subtle" 9 11 "encoding/base64" 10 12 "encoding/hex" 11 13 "errors" 12 14 "fmt" 15 + "hash" 13 16 "os" 14 17 "path/filepath" 15 18 "runtime" ··· 24 27 25 28 "github.com/dustin/go-humanize" 26 29 ) 27 - 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 30 35 31 // EncodeSha256 string to sha256 hex value. 36 32 func EncodeSha256(str string) string { ··· 62 58 } 63 59 64 60 // 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 { 66 62 if len(code) <= 18 { 67 63 return false 68 64 } 69 65 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 76 69 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 85 76 } 86 77 } 87 78 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))) 89 82 } 90 83 91 84 // TimeLimitCodeLength default value for time limit code 92 85 const TimeLimitCodeLength = 12 + 6 + 40 93 86 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" 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" 98 92 99 - var start, end time.Time 100 - var startStr, endStr string 101 - 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 106 97 } 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 + } 111 103 } 104 + startStr := start.Format(format) 105 + end := start.Add(time.Minute * time.Duration(minutes)) 112 106 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)) 120 112 121 113 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 + } 122 117 return code 123 118 } 124 119
+49 -40
modules/base/tool_test.go
··· 4 4 package base 5 5 6 6 import ( 7 + "crypto/sha1" 8 + "fmt" 7 9 "os" 8 10 "testing" 9 11 "time" 10 12 13 + "code.gitea.io/gitea/modules/setting" 14 + "code.gitea.io/gitea/modules/test" 15 + 11 16 "github.com/stretchr/testify/assert" 12 17 ) 13 - 14 - func TestEncodeSha1(t *testing.T) { 15 - assert.Equal(t, 16 - "8843d7f92416211de9ebb963ff4ce28125932878", 17 - EncodeSha1("foobar"), 18 - ) 19 - } 20 18 21 19 func TestEncodeSha256(t *testing.T) { 22 20 assert.Equal(t, ··· 46 44 } 47 45 48 46 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() 73 55 } 74 - } 56 + 57 + initGeneralSecret("KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko") 58 + now := time.Now() 75 59 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")) 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 + }) 80 67 81 - assert.Equal(t, result0, result1) 82 - assert.NotEqual(t, result0, result2) 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 + }) 83 72 84 - assert.True(t, len(result0) != 0) 85 - return result0 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 + }) 86 95 } 87 96 88 97 func TestFileSize(t *testing.T) {
+8
modules/git/utils.go
··· 4 4 package git 5 5 6 6 import ( 7 + "crypto/sha1" 8 + "encoding/hex" 7 9 "fmt" 8 10 "io" 9 11 "os" ··· 128 130 func (l *LimitedReaderCloser) Close() error { 129 131 return l.C.Close() 130 132 } 133 + 134 + func HashFilePathForWebUI(s string) string { 135 + h := sha1.New() 136 + _, _ = h.Write([]byte(s)) 137 + return hex.EncodeToString(h.Sum(nil)) 138 + }
+12 -1
modules/git/utils_test.go
··· 3 3 4 4 package git 5 5 6 - import "testing" 6 + import ( 7 + "testing" 8 + 9 + "github.com/stretchr/testify/assert" 10 + ) 7 11 8 12 // This file contains utility functions that are used across multiple tests, 9 13 // but not in production code. ··· 13 17 t.Skip("skipping because installed Git version doesn't support SHA256") 14 18 } 15 19 } 20 + 21 + func TestHashFilePathForWebUI(t *testing.T) { 22 + assert.Equal(t, 23 + "8843d7f92416211de9ebb963ff4ce28125932878", 24 + HashFilePathForWebUI("foobar"), 25 + ) 26 + }
+1 -1
routers/web/repo/compare.go
··· 931 931 } 932 932 } 933 933 ctx.Data["section"] = section 934 - ctx.Data["FileNameHash"] = base.EncodeSha1(filePath) 934 + ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath) 935 935 ctx.Data["AfterCommitID"] = commitID 936 936 ctx.Data["Anchor"] = anchor 937 937 ctx.HTML(http.StatusOK, tplBlobExcerpt)
+1 -2
services/gitdiff/gitdiff.go
··· 23 23 pull_model "code.gitea.io/gitea/models/pull" 24 24 user_model "code.gitea.io/gitea/models/user" 25 25 "code.gitea.io/gitea/modules/analyze" 26 - "code.gitea.io/gitea/modules/base" 27 26 "code.gitea.io/gitea/modules/charset" 28 27 "code.gitea.io/gitea/modules/git" 29 28 "code.gitea.io/gitea/modules/highlight" ··· 742 741 diffLineTypeBuffers[DiffLineAdd] = new(bytes.Buffer) 743 742 diffLineTypeBuffers[DiffLineDel] = new(bytes.Buffer) 744 743 for _, f := range diff.Files { 745 - f.NameHash = base.EncodeSha1(f.Name) 744 + f.NameHash = git.HashFilePathForWebUI(f.Name) 746 745 747 746 for _, buffer := range diffLineTypeBuffers { 748 747 buffer.Reset()