Switch plaintext scratch tokens to use hash instead (#4331)
This commit is contained in:
		
							parent
							
								
									ac968c3c6f
								
							
						
					
					
						commit
						adf3f004b6
					
				| 
						 | 
				
			
			@ -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")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue