Skip to content

Commit adf3f00

Browse files
Switch plaintext scratch tokens to use hash instead (#4331)
1 parent ac968c3 commit adf3f00

File tree

5 files changed

+118
-12
lines changed

5 files changed

+118
-12
lines changed

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ var migrations = []Migration{
194194
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
195195
// v70 -> v71
196196
NewMigration("add issue_dependencies", addIssueDependencies),
197+
// v70 -> v71
198+
NewMigration("protect each scratch token", addScratchHash),
197199
}
198200

199201
// Migrate database to current version

models/migrations/v71.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2018 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"crypto/sha256"
9+
"fmt"
10+
11+
"github.com/go-xorm/xorm"
12+
"golang.org/x/crypto/pbkdf2"
13+
14+
"code.gitea.io/gitea/modules/generate"
15+
"code.gitea.io/gitea/modules/util"
16+
)
17+
18+
func addScratchHash(x *xorm.Engine) error {
19+
// TwoFactor see models/twofactor.go
20+
type TwoFactor struct {
21+
ID int64 `xorm:"pk autoincr"`
22+
UID int64 `xorm:"UNIQUE"`
23+
Secret string
24+
ScratchToken string
25+
ScratchSalt string
26+
ScratchHash string
27+
LastUsedPasscode string `xorm:"VARCHAR(10)"`
28+
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
29+
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
30+
}
31+
32+
if err := x.Sync2(new(TwoFactor)); err != nil {
33+
return fmt.Errorf("Sync2: %v", err)
34+
}
35+
36+
sess := x.NewSession()
37+
defer sess.Close()
38+
39+
if err := sess.Begin(); err != nil {
40+
return err
41+
}
42+
43+
// transform all tokens to hashes
44+
const batchSize = 100
45+
for start := 0; ; start += batchSize {
46+
tfas := make([]*TwoFactor, 0, batchSize)
47+
if err := x.Limit(batchSize, start).Find(&tfas); err != nil {
48+
return err
49+
}
50+
if len(tfas) == 0 {
51+
break
52+
}
53+
54+
for _, tfa := range tfas {
55+
// generate salt
56+
salt, err := generate.GetRandomString(10)
57+
if err != nil {
58+
return err
59+
}
60+
tfa.ScratchSalt = salt
61+
tfa.ScratchHash = hashToken(tfa.ScratchToken, salt)
62+
63+
if _, err := sess.ID(tfa.ID).Cols("scratch_salt, scratch_hash").Update(tfa); err != nil {
64+
return fmt.Errorf("couldn't add in scratch_hash and scratch_salt: %v", err)
65+
}
66+
67+
}
68+
}
69+
70+
// Commit and begin new transaction for dropping columns
71+
if err := sess.Commit(); err != nil {
72+
return err
73+
}
74+
if err := sess.Begin(); err != nil {
75+
return err
76+
}
77+
78+
if err := dropTableColumns(sess, "two_factor", "scratch_token"); err != nil {
79+
return err
80+
}
81+
return sess.Commit()
82+
83+
}
84+
85+
func hashToken(token, salt string) string {
86+
tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New)
87+
return fmt.Sprintf("%x", tempHash)
88+
}

models/twofactor.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import (
99
"crypto/cipher"
1010
"crypto/md5"
1111
"crypto/rand"
12+
"crypto/sha256"
1213
"crypto/subtle"
1314
"encoding/base64"
1415
"errors"
16+
"fmt"
1517
"io"
1618

1719
"github.com/pquerna/otp/totp"
20+
"golang.org/x/crypto/pbkdf2"
1821

1922
"code.gitea.io/gitea/modules/generate"
2023
"code.gitea.io/gitea/modules/setting"
@@ -26,28 +29,36 @@ type TwoFactor struct {
2629
ID int64 `xorm:"pk autoincr"`
2730
UID int64 `xorm:"UNIQUE"`
2831
Secret string
29-
ScratchToken string
32+
ScratchSalt string
33+
ScratchHash string
3034
LastUsedPasscode string `xorm:"VARCHAR(10)"`
3135
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
3236
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
3337
}
3438

3539
// GenerateScratchToken recreates the scratch token the user is using.
36-
func (t *TwoFactor) GenerateScratchToken() error {
40+
func (t *TwoFactor) GenerateScratchToken() (string, error) {
3741
token, err := generate.GetRandomString(8)
3842
if err != nil {
39-
return err
43+
return "", err
4044
}
41-
t.ScratchToken = token
42-
return nil
45+
t.ScratchSalt, _ = generate.GetRandomString(10)
46+
t.ScratchHash = hashToken(token, t.ScratchSalt)
47+
return token, nil
48+
}
49+
50+
func hashToken(token, salt string) string {
51+
tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New)
52+
return fmt.Sprintf("%x", tempHash)
4353
}
4454

4555
// VerifyScratchToken verifies if the specified scratch token is valid.
4656
func (t *TwoFactor) VerifyScratchToken(token string) bool {
4757
if len(token) == 0 {
4858
return false
4959
}
50-
return subtle.ConstantTimeCompare([]byte(token), []byte(t.ScratchToken)) == 1
60+
tempHash := hashToken(token, t.ScratchSalt)
61+
return subtle.ConstantTimeCompare([]byte(t.ScratchHash), []byte(tempHash)) == 1
5162
}
5263

5364
func (t *TwoFactor) getEncryptionKey() []byte {
@@ -118,7 +129,7 @@ func aesDecrypt(key, text []byte) ([]byte, error) {
118129

119130
// NewTwoFactor creates a new two-factor authentication token.
120131
func NewTwoFactor(t *TwoFactor) error {
121-
err := t.GenerateScratchToken()
132+
_, err := t.GenerateScratchToken()
122133
if err != nil {
123134
return err
124135
}

routers/user/auth.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,11 @@ func TwoFactorScratchPost(ctx *context.Context, form auth.TwoFactorScratchAuthFo
306306
// Validate the passcode with the stored TOTP secret.
307307
if twofa.VerifyScratchToken(form.Token) {
308308
// Invalidate the scratch token.
309-
twofa.ScratchToken = ""
309+
_, err = twofa.GenerateScratchToken()
310+
if err != nil {
311+
ctx.ServerError("UserSignIn", err)
312+
return
313+
}
310314
if err = models.UpdateTwoFactor(twofa); err != nil {
311315
ctx.ServerError("UserSignIn", err)
312316
return

routers/user/setting/security_twofa.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
3232
return
3333
}
3434

35-
if err = t.GenerateScratchToken(); err != nil {
35+
token, err := t.GenerateScratchToken()
36+
if err != nil {
3637
ctx.ServerError("SettingsTwoFactor", err)
3738
return
3839
}
@@ -42,7 +43,7 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
4243
return
4344
}
4445

45-
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken))
46+
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
4647
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
4748
}
4849

@@ -170,7 +171,7 @@ func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
170171
ctx.ServerError("SettingsTwoFactor", err)
171172
return
172173
}
173-
err = t.GenerateScratchToken()
174+
token, err := t.GenerateScratchToken()
174175
if err != nil {
175176
ctx.ServerError("SettingsTwoFactor", err)
176177
return
@@ -183,6 +184,6 @@ func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
183184

184185
ctx.Session.Delete("twofaSecret")
185186
ctx.Session.Delete("twofaUri")
186-
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken))
187+
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
187188
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
188189
}

0 commit comments

Comments
 (0)