diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 48c4228fe1..b4e1a67187 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -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
diff --git a/models/migrations/v71.go b/models/migrations/v71.go
new file mode 100644
index 0000000000..c725908bd5
--- /dev/null
+++ b/models/migrations/v71.go
@@ -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)
+}
diff --git a/models/twofactor.go b/models/twofactor.go
index 5f3c6efc21..be37c50b46 100644
--- a/models/twofactor.go
+++ b/models/twofactor.go
@@ -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
 	}
diff --git a/routers/user/auth.go b/routers/user/auth.go
index b24c56745d..e99f9d5de1 100644
--- a/routers/user/auth.go
+++ b/routers/user/auth.go
@@ -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
diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go
index cb61b9e270..3a590f0b08 100644
--- a/routers/user/setting/security_twofa.go
+++ b/routers/user/setting/security_twofa.go
@@ -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")
 }