mirror of https://github.com/gogs/gogs.git
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 RandomCharspull/6102/head
parent
659acd48b1
commit
cb439a126a
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue