@@ -194,6 +194,8 @@ var migrations = []Migration{ | |||
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable), | |||
// v70 -> v71 | |||
NewMigration("add issue_dependencies", addIssueDependencies), | |||
// v70 -> v71 | |||
NewMigration("protect each scratch token", addScratchHash), | |||
} | |||
// Migrate database to current version |
@@ -0,0 +1,88 @@ | |||
// Copyright 2018 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package migrations | |||
import ( | |||
"crypto/sha256" | |||
"fmt" | |||
"github.com/go-xorm/xorm" | |||
"golang.org/x/crypto/pbkdf2" | |||
"code.gitea.io/gitea/modules/generate" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
func addScratchHash(x *xorm.Engine) error { | |||
// TwoFactor see models/twofactor.go | |||
type TwoFactor struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
UID int64 `xorm:"UNIQUE"` | |||
Secret string | |||
ScratchToken string | |||
ScratchSalt string | |||
ScratchHash string | |||
LastUsedPasscode string `xorm:"VARCHAR(10)"` | |||
CreatedUnix util.TimeStamp `xorm:"INDEX created"` | |||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | |||
} | |||
if err := x.Sync2(new(TwoFactor)); err != nil { | |||
return fmt.Errorf("Sync2: %v", err) | |||
} | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
if err := sess.Begin(); err != nil { | |||
return err | |||
} | |||
// transform all tokens to hashes | |||
const batchSize = 100 | |||
for start := 0; ; start += batchSize { | |||
tfas := make([]*TwoFactor, 0, batchSize) | |||
if err := x.Limit(batchSize, start).Find(&tfas); err != nil { | |||
return err | |||
} | |||
if len(tfas) == 0 { | |||
break | |||
} | |||
for _, tfa := range tfas { | |||
// generate salt | |||
salt, err := generate.GetRandomString(10) | |||
if err != nil { | |||
return err | |||
} | |||
tfa.ScratchSalt = salt | |||
tfa.ScratchHash = hashToken(tfa.ScratchToken, salt) | |||
if _, err := sess.ID(tfa.ID).Cols("scratch_salt, scratch_hash").Update(tfa); err != nil { | |||
return fmt.Errorf("couldn't add in scratch_hash and scratch_salt: %v", err) | |||
} | |||
} | |||
} | |||
// Commit and begin new transaction for dropping columns | |||
if err := sess.Commit(); err != nil { | |||
return err | |||
} | |||
if err := sess.Begin(); err != nil { | |||
return err | |||
} | |||
if err := dropTableColumns(sess, "two_factor", "scratch_token"); err != nil { | |||
return err | |||
} | |||
return sess.Commit() | |||
} | |||
func hashToken(token, salt string) string { | |||
tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New) | |||
return fmt.Sprintf("%x", tempHash) | |||
} |
@@ -9,12 +9,15 @@ import ( | |||
"crypto/cipher" | |||
"crypto/md5" | |||
"crypto/rand" | |||
"crypto/sha256" | |||
"crypto/subtle" | |||
"encoding/base64" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"github.com/pquerna/otp/totp" | |||
"golang.org/x/crypto/pbkdf2" | |||
"code.gitea.io/gitea/modules/generate" | |||
"code.gitea.io/gitea/modules/setting" | |||
@@ -26,20 +29,27 @@ type TwoFactor struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
UID int64 `xorm:"UNIQUE"` | |||
Secret string | |||
ScratchToken string | |||
ScratchSalt string | |||
ScratchHash string | |||
LastUsedPasscode string `xorm:"VARCHAR(10)"` | |||
CreatedUnix util.TimeStamp `xorm:"INDEX created"` | |||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | |||
} | |||
// GenerateScratchToken recreates the scratch token the user is using. | |||
func (t *TwoFactor) GenerateScratchToken() error { | |||
func (t *TwoFactor) GenerateScratchToken() (string, error) { | |||
token, err := generate.GetRandomString(8) | |||
if err != nil { | |||
return err | |||
return "", err | |||
} | |||
t.ScratchToken = token | |||
return nil | |||
t.ScratchSalt, _ = generate.GetRandomString(10) | |||
t.ScratchHash = hashToken(token, t.ScratchSalt) | |||
return token, nil | |||
} | |||
func hashToken(token, salt string) string { | |||
tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New) | |||
return fmt.Sprintf("%x", tempHash) | |||
} | |||
// VerifyScratchToken verifies if the specified scratch token is valid. | |||
@@ -47,7 +57,8 @@ func (t *TwoFactor) VerifyScratchToken(token string) bool { | |||
if len(token) == 0 { | |||
return false | |||
} | |||
return subtle.ConstantTimeCompare([]byte(token), []byte(t.ScratchToken)) == 1 | |||
tempHash := hashToken(token, t.ScratchSalt) | |||
return subtle.ConstantTimeCompare([]byte(t.ScratchHash), []byte(tempHash)) == 1 | |||
} | |||
func (t *TwoFactor) getEncryptionKey() []byte { | |||
@@ -118,7 +129,7 @@ func aesDecrypt(key, text []byte) ([]byte, error) { | |||
// NewTwoFactor creates a new two-factor authentication token. | |||
func NewTwoFactor(t *TwoFactor) error { | |||
err := t.GenerateScratchToken() | |||
_, err := t.GenerateScratchToken() | |||
if err != nil { | |||
return err | |||
} |
@@ -306,7 +306,11 @@ func TwoFactorScratchPost(ctx *context.Context, form auth.TwoFactorScratchAuthFo | |||
// Validate the passcode with the stored TOTP secret. | |||
if twofa.VerifyScratchToken(form.Token) { | |||
// Invalidate the scratch token. | |||
twofa.ScratchToken = "" | |||
_, err = twofa.GenerateScratchToken() | |||
if err != nil { | |||
ctx.ServerError("UserSignIn", err) | |||
return | |||
} | |||
if err = models.UpdateTwoFactor(twofa); err != nil { | |||
ctx.ServerError("UserSignIn", err) | |||
return |
@@ -32,7 +32,8 @@ func RegenerateScratchTwoFactor(ctx *context.Context) { | |||
return | |||
} | |||
if err = t.GenerateScratchToken(); err != nil { | |||
token, err := t.GenerateScratchToken() | |||
if err != nil { | |||
ctx.ServerError("SettingsTwoFactor", err) | |||
return | |||
} | |||
@@ -42,7 +43,7 @@ func RegenerateScratchTwoFactor(ctx *context.Context) { | |||
return | |||
} | |||
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken)) | |||
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token)) | |||
ctx.Redirect(setting.AppSubURL + "/user/settings/security") | |||
} | |||
@@ -170,7 +171,7 @@ func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) { | |||
ctx.ServerError("SettingsTwoFactor", err) | |||
return | |||
} | |||
err = t.GenerateScratchToken() | |||
token, err := t.GenerateScratchToken() | |||
if err != nil { | |||
ctx.ServerError("SettingsTwoFactor", err) | |||
return | |||
@@ -183,6 +184,6 @@ func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) { | |||
ctx.Session.Delete("twofaSecret") | |||
ctx.Session.Delete("twofaUri") | |||
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken)) | |||
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token)) | |||
ctx.Redirect(setting.AppSubURL + "/user/settings/security") | |||
} |