Browse Source

db: add tests for two factors (#6099)

* Rename to TwoFactors.Create

* Use GORM to execute queries

* TwoFactor.GetByUserID

* Add tests

* Fix failing tests

* Add MD5 tests

* Add tests for RandomChars
ᴜɴᴋɴᴡᴏɴ 4 years ago
parent
commit
cb439a126a

+ 56 - 0
internal/cryptoutil/aes.go

@@ -0,0 +1,56 @@
+// Copyright 2020 The Gogs 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 cryptoutil
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"errors"
+)
+
+// AESGCMEncrypt encrypts plaintext with the given key using AES in GCM mode.
+func AESGCMEncrypt(key, plaintext []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	nonce := make([]byte, gcm.NonceSize())
+	if _, err := rand.Read(nonce); err != nil {
+		return nil, err
+	}
+
+	ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
+	return append(nonce, ciphertext...), nil
+}
+
+// AESGCMDecrypt decrypts ciphertext with the given key using AES in GCM mode.
+func AESGCMDecrypt(key, ciphertext []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	size := gcm.NonceSize()
+	if len(ciphertext)-size <= 0 {
+		return nil, errors.New("ciphertext is empty")
+	}
+
+	nonce := ciphertext[:size]
+	ciphertext = ciphertext[size:]
+
+	return gcm.Open(nil, nonce, ciphertext, nil)
+}

+ 34 - 0
internal/cryptoutil/aes_test.go

@@ -0,0 +1,34 @@
+// Copyright 2020 The Gogs 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 cryptoutil
+
+import (
+	"crypto/rand"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAESGCM(t *testing.T) {
+	key := make([]byte, 16) // AES-128
+	_, err := rand.Read(key)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	plaintext := []byte("this will be encrypted")
+
+	encrypted, err := AESGCMEncrypt(key, plaintext)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	decrypted, err := AESGCMDecrypt(key, encrypted)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, plaintext, decrypted)
+}

+ 22 - 0
internal/cryptoutil/md5.go

@@ -0,0 +1,22 @@
+// Copyright 2020 The Gogs 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 cryptoutil
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+)
+
+// MD5 encodes string to hexadecimal of MD5 checksum.
+func MD5(str string) string {
+	return hex.EncodeToString(MD5Bytes(str))
+}
+
+// MD5Bytes encodes string to MD5 checksum.
+func MD5Bytes(str string) []byte {
+	m := md5.New()
+	_, _ = m.Write([]byte(str))
+	return m.Sum(nil)
+}

+ 27 - 0
internal/cryptoutil/md5_test.go

@@ -0,0 +1,27 @@
+// Copyright 2020 The Gogs 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 cryptoutil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMD5(t *testing.T) {
+	tests := []struct {
+		input  string
+		output string
+	}{
+		{input: "", output: "d41d8cd98f00b204e9800998ecf8427e"},
+		{input: "The quick brown fox jumps over the lazy dog", output: "9e107d9d372bb6826bd81d3542a419d6"},
+		{input: "The quick brown fox jumps over the lazy dog.", output: "e4d909c290d0fb1ca068ffaddf22cbd0"},
+	}
+	for _, test := range tests {
+		t.Run(test.input, func(t *testing.T) {
+			assert.Equal(t, test.output, MD5(test.input))
+		})
+	}
+}

+ 0 - 20
internal/db/errors/two_factor.go

@@ -1,20 +0,0 @@
-// Copyright 2017 The Gogs 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 errors
-
-import "fmt"
-
-type TwoFactorNotFound struct {
-	UserID int64
-}
-
-func IsTwoFactorNotFound(err error) bool {
-	_, ok := err.(TwoFactorNotFound)
-	return ok
-}
-
-func (err TwoFactorNotFound) Error() string {
-	return fmt.Sprintf("two-factor authentication does not found [user_id: %d]", err.UserID)
-}

+ 3 - 3
internal/db/migrations/migrations.go

@@ -13,7 +13,7 @@ import (
 	log "unknwon.dev/clog/v2"
 	"xorm.io/xorm"
 
-	"gogs.io/gogs/internal/tool"
+	"gogs.io/gogs/internal/strutil"
 )
 
 const _MIN_DB_VER = 10
@@ -156,10 +156,10 @@ func generateOrgRandsAndSalt(x *xorm.Engine) (err error) {
 	}
 
 	for _, org := range orgs {
-		if org.Rands, err = tool.RandomString(10); err != nil {
+		if org.Rands, err = strutil.RandomChars(10); err != nil {
 			return err
 		}
-		if org.Salt, err = tool.RandomString(10); err != nil {
+		if org.Salt, err = strutil.RandomChars(10); err != nil {
 			return err
 		}
 		if _, err = sess.ID(org.ID).Update(org); err != nil {

+ 10 - 0
internal/db/mocks.go

@@ -180,9 +180,19 @@ func SetMockReposStore(t *testing.T, mock ReposStore) {
 var _ TwoFactorsStore = (*MockTwoFactorsStore)(nil)
 
 type MockTwoFactorsStore struct {
+	MockCreate        func(userID int64, key, secret string) error
+	MockGetByUserID   func(userID int64) (*TwoFactor, error)
 	MockIsUserEnabled func(userID int64) bool
 }
 
+func (m *MockTwoFactorsStore) Create(userID int64, key, secret string) error {
+	return m.MockCreate(userID, key, secret)
+}
+
+func (m *MockTwoFactorsStore) GetByUserID(userID int64) (*TwoFactor, error) {
+	return m.MockGetByUserID(userID)
+}
+
 func (m *MockTwoFactorsStore) IsUserEnabled(userID int64) bool {
 	return m.MockIsUserEnabled(userID)
 }

+ 2 - 1
internal/db/repo_editor.go

@@ -22,6 +22,7 @@ import (
 	"github.com/gogs/git-module"
 
 	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/cryptoutil"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/osutil"
@@ -56,7 +57,7 @@ func ComposeHookEnvs(opts ComposeHookEnvsOptions) []string {
 		ENV_AUTH_USER_NAME + "=" + opts.AuthUser.Name,
 		ENV_AUTH_USER_EMAIL + "=" + opts.AuthUser.Email,
 		ENV_REPO_OWNER_NAME + "=" + opts.OwnerName,
-		ENV_REPO_OWNER_SALT_MD5 + "=" + tool.MD5(opts.OwnerSalt),
+		ENV_REPO_OWNER_SALT_MD5 + "=" + cryptoutil.MD5(opts.OwnerSalt),
 		ENV_REPO_ID + "=" + com.ToStr(opts.RepoID),
 		ENV_REPO_NAME + "=" + opts.RepoName,
 		ENV_REPO_CUSTOM_HOOKS_PATH + "=" + filepath.Join(opts.RepoPath, "custom_hooks"),

+ 6 - 81
internal/db/two_factor.go

@@ -7,38 +7,24 @@ package db
 import (
 	"encoding/base64"
 	"fmt"
-	"strings"
 	"time"
 
 	"github.com/pquerna/otp/totp"
 	"github.com/unknwon/com"
-	"xorm.io/xorm"
 
 	"gogs.io/gogs/internal/conf"
-	"gogs.io/gogs/internal/db/errors"
-	"gogs.io/gogs/internal/tool"
+	"gogs.io/gogs/internal/cryptoutil"
 )
 
-// TwoFactor represents a two-factor authentication token.
+// TwoFactor is a 2FA token of a user.
 type TwoFactor struct {
 	ID          int64
-	UserID      int64 `xorm:"UNIQUE"`
+	UserID      int64 `xorm:"UNIQUE" gorm:"UNIQUE"`
 	Secret      string
-	Created     time.Time `xorm:"-" json:"-"`
+	Created     time.Time `xorm:"-" gorm:"-" json:"-"`
 	CreatedUnix int64
 }
 
-func (t *TwoFactor) BeforeInsert() {
-	t.CreatedUnix = time.Now().Unix()
-}
-
-func (t *TwoFactor) AfterSet(colName string, _ xorm.Cell) {
-	switch colName {
-	case "created_unix":
-		t.Created = time.Unix(t.CreatedUnix, 0).Local()
-	}
-}
-
 // ValidateTOTP returns true if given passcode is valid for two-factor authentication token.
 // It also returns possible validation error.
 func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
@@ -46,74 +32,13 @@ func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
 	if err != nil {
 		return false, fmt.Errorf("DecodeString: %v", err)
 	}
-	decryptSecret, err := com.AESGCMDecrypt(tool.MD5Bytes(conf.Security.SecretKey), secret)
+	decryptSecret, err := com.AESGCMDecrypt(cryptoutil.MD5Bytes(conf.Security.SecretKey), secret)
 	if err != nil {
 		return false, fmt.Errorf("AESGCMDecrypt: %v", err)
 	}
 	return totp.Validate(passcode, string(decryptSecret)), nil
 }
 
-func generateRecoveryCodes(userID int64) ([]*TwoFactorRecoveryCode, error) {
-	recoveryCodes := make([]*TwoFactorRecoveryCode, 10)
-	for i := 0; i < 10; i++ {
-		code, err := tool.RandomString(10)
-		if err != nil {
-			return nil, fmt.Errorf("RandomString: %v", err)
-		}
-		recoveryCodes[i] = &TwoFactorRecoveryCode{
-			UserID: userID,
-			Code:   strings.ToLower(code[:5] + "-" + code[5:]),
-		}
-	}
-	return recoveryCodes, nil
-}
-
-// NewTwoFactor creates a new two-factor authentication token and recovery codes for given user.
-func NewTwoFactor(userID int64, secret string) error {
-	t := &TwoFactor{
-		UserID: userID,
-	}
-
-	// Encrypt secret
-	encryptSecret, err := com.AESGCMEncrypt(tool.MD5Bytes(conf.Security.SecretKey), []byte(secret))
-	if err != nil {
-		return fmt.Errorf("AESGCMEncrypt: %v", err)
-	}
-	t.Secret = base64.StdEncoding.EncodeToString(encryptSecret)
-
-	recoveryCodes, err := generateRecoveryCodes(userID)
-	if err != nil {
-		return fmt.Errorf("generateRecoveryCodes: %v", err)
-	}
-
-	sess := x.NewSession()
-	defer sess.Close()
-	if err = sess.Begin(); err != nil {
-		return err
-	}
-
-	if _, err = sess.Insert(t); err != nil {
-		return fmt.Errorf("insert two-factor: %v", err)
-	} else if _, err = sess.Insert(recoveryCodes); err != nil {
-		return fmt.Errorf("insert recovery codes: %v", err)
-	}
-
-	return sess.Commit()
-}
-
-// GetTwoFactorByUserID returns two-factor authentication token of given user.
-func GetTwoFactorByUserID(userID int64) (*TwoFactor, error) {
-	t := new(TwoFactor)
-	has, err := x.Where("user_id = ?", userID).Get(t)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, errors.TwoFactorNotFound{UserID: userID}
-	}
-
-	return t, nil
-}
-
 // DeleteTwoFactor removes two-factor authentication token and recovery codes of given user.
 func DeleteTwoFactor(userID int64) (err error) {
 	sess := x.NewSession()
@@ -152,7 +77,7 @@ func deleteRecoveryCodesByUserID(e Engine, userID int64) error {
 
 // RegenerateRecoveryCodes regenerates new set of recovery codes for given user.
 func RegenerateRecoveryCodes(userID int64) error {
-	recoveryCodes, err := generateRecoveryCodes(userID)
+	recoveryCodes, err := generateRecoveryCodes(userID, 10)
 	if err != nil {
 		return fmt.Errorf("generateRecoveryCodes: %v", err)
 	}

+ 110 - 0
internal/db/two_factors.go

@@ -5,24 +5,118 @@
 package db
 
 import (
+	"encoding/base64"
+	"fmt"
+	"strings"
+	"time"
+
 	"github.com/jinzhu/gorm"
+	"github.com/pkg/errors"
 	log "unknwon.dev/clog/v2"
+
+	"gogs.io/gogs/internal/cryptoutil"
+	"gogs.io/gogs/internal/errutil"
+	"gogs.io/gogs/internal/strutil"
 )
 
 // TwoFactorsStore is the persistent interface for 2FA.
 //
 // NOTE: All methods are sorted in alphabetical order.
 type TwoFactorsStore interface {
+	// Create creates a new 2FA token and recovery codes for given user.
+	// The "key" is used to encrypt and later decrypt given "secret",
+	// which should be configured in site-level and change of the "key"
+	// will break all existing 2FA tokens.
+	Create(userID int64, key, secret string) error
+	// GetByUserID returns the 2FA token of given user.
+	// It returns ErrTwoFactorNotFound when not found.
+	GetByUserID(userID int64) (*TwoFactor, error)
 	// IsUserEnabled returns true if the user has enabled 2FA.
 	IsUserEnabled(userID int64) bool
 }
 
 var TwoFactors TwoFactorsStore
 
+// NOTE: This is a GORM create hook.
+func (t *TwoFactor) BeforeCreate() {
+	t.CreatedUnix = gorm.NowFunc().Unix()
+}
+
+// NOTE: This is a GORM query hook.
+func (t *TwoFactor) AfterFind() {
+	t.Created = time.Unix(t.CreatedUnix, 0).Local()
+}
+
+var _ TwoFactorsStore = (*twoFactors)(nil)
+
 type twoFactors struct {
 	*gorm.DB
 }
 
+func (db *twoFactors) Create(userID int64, key, secret string) error {
+	encrypted, err := cryptoutil.AESGCMEncrypt(cryptoutil.MD5Bytes(key), []byte(secret))
+	if err != nil {
+		return errors.Wrap(err, "encrypt secret")
+	}
+	tf := &TwoFactor{
+		UserID: userID,
+		Secret: base64.StdEncoding.EncodeToString(encrypted),
+	}
+
+	recoveryCodes, err := generateRecoveryCodes(userID, 10)
+	if err != nil {
+		return errors.Wrap(err, "generate recovery codes")
+	}
+
+	vals := make([]string, 0, len(recoveryCodes))
+	items := make([]interface{}, 0, len(recoveryCodes)*2)
+	for _, code := range recoveryCodes {
+		vals = append(vals, "(?, ?)")
+		items = append(items, code.UserID, code.Code)
+	}
+
+	return db.Transaction(func(tx *gorm.DB) error {
+		err := tx.Create(tf).Error
+		if err != nil {
+			return err
+		}
+
+		sql := "INSERT INTO two_factor_recovery_code (user_id, code) VALUES " + strings.Join(vals, ", ")
+		return tx.Exec(sql, items...).Error
+	})
+}
+
+var _ errutil.NotFound = (*ErrTwoFactorNotFound)(nil)
+
+type ErrTwoFactorNotFound struct {
+	args errutil.Args
+}
+
+func IsErrTwoFactorNotFound(err error) bool {
+	_, ok := err.(ErrTwoFactorNotFound)
+	return ok
+}
+
+func (err ErrTwoFactorNotFound) Error() string {
+	return fmt.Sprintf("2FA does not found: %v", err.args)
+}
+
+func (ErrTwoFactorNotFound) NotFound() bool {
+	return true
+}
+
+func (db *twoFactors) GetByUserID(userID int64) (*TwoFactor, error) {
+	tf := new(TwoFactor)
+	err := db.Where("user_id = ?", userID).First(tf).Error
+	if err != nil {
+		if gorm.IsRecordNotFoundError(err) {
+			return nil, ErrTwoFactorNotFound{args: errutil.Args{"userID": userID}}
+		}
+		return nil, err
+	}
+	return tf, nil
+}
+
 func (db *twoFactors) IsUserEnabled(userID int64) bool {
 	var count int64
 	err := db.Model(new(TwoFactor)).Where("user_id = ?", userID).Count(&count).Error
@@ -31,3 +125,19 @@ func (db *twoFactors) IsUserEnabled(userID int64) bool {
 	}
 	return count > 0
 }
+
+// generateRecoveryCodes generates N number of recovery codes for 2FA.
+func generateRecoveryCodes(userID int64, n int) ([]*TwoFactorRecoveryCode, error) {
+	recoveryCodes := make([]*TwoFactorRecoveryCode, n)
+	for i := 0; i < n; i++ {
+		code, err := strutil.RandomChars(10)
+		if err != nil {
+			return nil, errors.Wrap(err, "generate random characters")
+		}
+		recoveryCodes[i] = &TwoFactorRecoveryCode{
+			UserID: userID,
+			Code:   strings.ToLower(code[:5] + "-" + code[5:]),
+		}
+	}
+	return recoveryCodes, nil
+}

+ 100 - 0
internal/db/two_factors_test.go

@@ -0,0 +1,100 @@
+// Copyright 2020 The Gogs 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 db
+
+import (
+	"testing"
+	"time"
+
+	"github.com/jinzhu/gorm"
+	"github.com/stretchr/testify/assert"
+
+	"gogs.io/gogs/internal/errutil"
+)
+
+func Test_twoFactors(t *testing.T) {
+	if testing.Short() {
+		t.Skip()
+	}
+
+	t.Parallel()
+
+	tables := []interface{}{new(TwoFactor), new(TwoFactorRecoveryCode)}
+	db := &twoFactors{
+		DB: initTestDB(t, "twoFactors", tables...),
+	}
+
+	for _, tc := range []struct {
+		name string
+		test func(*testing.T, *twoFactors)
+	}{
+		{"Create", test_twoFactors_Create},
+		{"GetByUserID", test_twoFactors_GetByUserID},
+		{"IsUserEnabled", test_twoFactors_IsUserEnabled},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Cleanup(func() {
+				err := clearTables(db.DB, tables...)
+				if err != nil {
+					t.Fatal(err)
+				}
+			})
+			tc.test(t, db)
+		})
+	}
+}
+
+func test_twoFactors_Create(t *testing.T, db *twoFactors) {
+	// Create a 2FA token
+	err := db.Create(1, "secure-key", "secure-secret")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Get it back and check the Created field
+	tf, err := db.GetByUserID(1)
+	if err != nil {
+		t.Fatal(err)
+	}
+	assert.Equal(t, gorm.NowFunc().Format(time.RFC3339), tf.Created.Format(time.RFC3339))
+
+	// Verify there are 10 recover codes generated
+	var count int64
+	err = db.Model(new(TwoFactorRecoveryCode)).Count(&count).Error
+	if err != nil {
+		t.Fatal(err)
+	}
+	assert.Equal(t, int64(10), count)
+}
+
+func test_twoFactors_GetByUserID(t *testing.T, db *twoFactors) {
+	// Create a 2FA token for user 1
+	err := db.Create(1, "secure-key", "secure-secret")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// We should be able to get it back
+	_, err = db.GetByUserID(1)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Try to get a non-existent 2FA token
+	_, err = db.GetByUserID(2)
+	expErr := ErrTwoFactorNotFound{args: errutil.Args{"userID": int64(2)}}
+	assert.Equal(t, expErr, err)
+}
+
+func test_twoFactors_IsUserEnabled(t *testing.T, db *twoFactors) {
+	// Create a 2FA token for user 1
+	err := db.Create(1, "secure-key", "secure-secret")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.True(t, db.IsUserEnabled(1))
+	assert.False(t, db.IsUserEnabled(2))
+}

+ 3 - 2
internal/db/user.go

@@ -32,6 +32,7 @@ import (
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/errutil"
+	"gogs.io/gogs/internal/strutil"
 	"gogs.io/gogs/internal/tool"
 )
 
@@ -483,9 +484,9 @@ func IsUserExist(uid int64, name string) (bool, error) {
 	return x.Where("id != ?", uid).Get(&User{LowerName: strings.ToLower(name)})
 }
 
-// GetUserSalt returns a ramdom user salt token.
+// GetUserSalt returns a random user salt token.
 func GetUserSalt() (string, error) {
-	return tool.RandomString(10)
+	return strutil.RandomChars(10)
 }
 
 // NewGhostUser creates and returns a fake user for someone who has deleted his/her account.

+ 2 - 2
internal/route/install.go

@@ -27,8 +27,8 @@ import (
 	"gogs.io/gogs/internal/markup"
 	"gogs.io/gogs/internal/osutil"
 	"gogs.io/gogs/internal/ssh"
+	"gogs.io/gogs/internal/strutil"
 	"gogs.io/gogs/internal/template/highlight"
-	"gogs.io/gogs/internal/tool"
 )
 
 const (
@@ -365,7 +365,7 @@ func InstallPost(c *context.Context, f form.Install) {
 	cfg.Section("log").Key("ROOT_PATH").SetValue(f.LogRootPath)
 
 	cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
-	secretKey, err := tool.RandomString(15)
+	secretKey, err := strutil.RandomChars(15)
 	if err != nil {
 		c.RenderWithErr(c.Tr("install.secret_key_failed", err), INSTALL, &f)
 		return

+ 2 - 2
internal/route/repo/tasks.go

@@ -10,8 +10,8 @@ import (
 	"gopkg.in/macaron.v1"
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/cryptoutil"
 	"gogs.io/gogs/internal/db"
-	"gogs.io/gogs/internal/tool"
 )
 
 func TriggerTask(c *macaron.Context) {
@@ -39,7 +39,7 @@ func TriggerTask(c *macaron.Context) {
 
 	// 🚨 SECURITY: No need to check existence of the repository if the client
 	// can't even get the valid secret. Mostly likely not a legitimate request.
-	if secret != tool.MD5(owner.Salt) {
+	if secret != cryptoutil.MD5(owner.Salt) {
 		c.Error(http.StatusBadRequest, "Invalid secret")
 		return
 	}

+ 1 - 1
internal/route/user/auth.go

@@ -209,7 +209,7 @@ func LoginTwoFactorPost(c *context.Context) {
 		return
 	}
 
-	t, err := db.GetTwoFactorByUserID(userID)
+	t, err := db.TwoFactors.GetByUserID(userID)
 	if err != nil {
 		c.Error(err, "get two factor by user ID")
 		return

+ 5 - 4
internal/route/user/setting.go

@@ -20,6 +20,7 @@ import (
 
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/cryptoutil"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/email"
@@ -118,7 +119,7 @@ func SettingsPost(c *context.Context, f form.UpdateProfile) {
 func UpdateAvatarSetting(c *context.Context, f form.Avatar, ctxUser *db.User) error {
 	ctxUser.UseCustomAvatar = f.Source == form.AVATAR_LOCAL
 	if len(f.Gravatar) > 0 {
-		ctxUser.Avatar = tool.MD5(f.Gravatar)
+		ctxUser.Avatar = cryptoutil.MD5(f.Gravatar)
 		ctxUser.AvatarEmail = f.Gravatar
 	}
 
@@ -381,8 +382,8 @@ func SettingsSecurity(c *context.Context) {
 	c.Title("settings.security")
 	c.PageIs("SettingsSecurity")
 
-	t, err := db.GetTwoFactorByUserID(c.UserID())
-	if err != nil && !errors.IsTwoFactorNotFound(err) {
+	t, err := db.TwoFactors.GetByUserID(c.UserID())
+	if err != nil && !db.IsErrTwoFactorNotFound(err) {
 		c.Errorf(err, "get two factor by user ID")
 		return
 	}
@@ -449,7 +450,7 @@ func SettingsTwoFactorEnablePost(c *context.Context) {
 		return
 	}
 
-	if err := db.NewTwoFactor(c.UserID(), secret); err != nil {
+	if err := db.TwoFactors.Create(c.UserID(), conf.Security.SecretKey, secret); err != nil {
 		c.Flash.Error(c.Tr("settings.two_factor_enable_error", err))
 		c.RedirectSubpath("/user/settings/security/two_factor_enable")
 		return

+ 29 - 0
internal/strutil/strutil.go

@@ -5,6 +5,8 @@
 package strutil
 
 import (
+	"crypto/rand"
+	"math/big"
 	"unicode"
 )
 
@@ -15,3 +17,30 @@ func ToUpperFirst(s string) string {
 	}
 	return ""
 }
+
+// RandomChars returns a generated string in given number of random characters.
+func RandomChars(n int) (string, error) {
+	const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+	randomInt := func(max *big.Int) (int, error) {
+		r, err := rand.Int(rand.Reader, max)
+		if err != nil {
+			return 0, err
+		}
+
+		return int(r.Int64()), nil
+	}
+
+	buffer := make([]byte, n)
+	max := big.NewInt(int64(len(alphanum)))
+	for i := 0; i < n; i++ {
+		index, err := randomInt(max)
+		if err != nil {
+			return "", err
+		}
+
+		buffer[i] = alphanum[index]
+	}
+
+	return string(buffer), nil
+}

+ 14 - 0
internal/strutil/strutil_test.go

@@ -41,3 +41,17 @@ func TestToUpperFirst(t *testing.T) {
 		})
 	}
 }
+
+func TestRandomChars(t *testing.T) {
+	cache := make(map[string]bool)
+	for i := 0; i < 100; i++ {
+		chars, err := RandomChars(10)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if cache[chars] {
+			t.Fatalf("Duplicated chars %q", chars)
+		}
+		cache[chars] = true
+	}
+}

+ 1 - 51
internal/tool/tool.go

@@ -6,13 +6,11 @@ package tool
 
 import (
 	"crypto/md5"
-	"crypto/rand"
 	"crypto/sha1"
 	"encoding/base64"
 	"encoding/hex"
 	"fmt"
 	"html/template"
-	"math/big"
 	"strings"
 	"time"
 	"unicode"
@@ -27,22 +25,8 @@ import (
 	"gogs.io/gogs/internal/conf"
 )
 
-// MD5Bytes encodes string to MD5 bytes.
-// TODO: Move to hashutil.MD5Bytes.
-func MD5Bytes(str string) []byte {
-	m := md5.New()
-	_, _ = m.Write([]byte(str))
-	return m.Sum(nil)
-}
-
-// MD5 encodes string to MD5 hex value.
-// TODO: Move to hashutil.MD5.
-func MD5(str string) string {
-	return hex.EncodeToString(MD5Bytes(str))
-}
-
 // SHA1 encodes string to SHA1 hex value.
-// TODO: Move to hashutil.SHA1.
+// TODO: Move to cryptoutil.SHA1.
 func SHA1(str string) string {
 	h := sha1.New()
 	_, _ = h.Write([]byte(str))
@@ -86,40 +70,6 @@ func BasicAuthDecode(encoded string) (string, string, error) {
 	return auth[0], auth[1], nil
 }
 
-// BasicAuthEncode encodes username and password in HTTP Basic Authentication format.
-func BasicAuthEncode(username, password string) string {
-	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
-}
-
-const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-
-// RandomString returns generated random string in given length of characters.
-// It also returns possible error during generation.
-func RandomString(n int) (string, error) {
-	buffer := make([]byte, n)
-	max := big.NewInt(int64(len(alphanum)))
-
-	for i := 0; i < n; i++ {
-		index, err := randomInt(max)
-		if err != nil {
-			return "", err
-		}
-
-		buffer[i] = alphanum[index]
-	}
-
-	return string(buffer), nil
-}
-
-func randomInt(max *big.Int) (int, error) {
-	rand, err := rand.Int(rand.Reader, max)
-	if err != nil {
-		return 0, err
-	}
-
-	return int(rand.Int64()), nil
-}
-
 // verify time limit code
 func VerifyTimeLimitCode(data string, minutes int, code string) bool {
 	if len(code) <= 18 {