diff options
author | Andrew <write@imaginarycode.com> | 2017-01-15 21:14:29 -0500 |
---|---|---|
committer | Lunny Xiao <xiaolunwen@gmail.com> | 2017-01-16 10:14:29 +0800 |
commit | 6dd096b7f08799ff27d9e34356fb1163ca10f388 (patch) | |
tree | e5823a27df5def081c9c39f5fac1424a81875f6d /models | |
parent | 64375d875b4d46a6081026290da8efd82c84b25f (diff) | |
download | gitea-6dd096b7f08799ff27d9e34356fb1163ca10f388.tar.gz gitea-6dd096b7f08799ff27d9e34356fb1163ca10f388.zip |
Two factor authentication support (#630)
* Initial commit for 2FA support
Signed-off-by: Andrew <write@imaginarycode.com>
* Add vendored files
* Add missing depends
* A few clean ups
* Added improvements, proper encryption
* Better encryption key
* Simplify "key" generation
* Make 2FA enrollment page more robust
* Fix typo
* Rename twofa/2FA to TwoFactor
* UNIQUE INDEX -> UNIQUE
Diffstat (limited to 'models')
-rw-r--r-- | models/error.go | 19 | ||||
-rw-r--r-- | models/models.go | 1 | ||||
-rw-r--r-- | models/twofactor.go | 141 |
3 files changed, 161 insertions, 0 deletions
diff --git a/models/error.go b/models/error.go index edbb6003e9..e0d33df6dc 100644 --- a/models/error.go +++ b/models/error.go @@ -787,6 +787,25 @@ func (err ErrTeamAlreadyExist) Error() string { return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name) } +// +// Two-factor authentication +// + +// ErrTwoFactorNotEnrolled indicates that a user is not enrolled in two-factor authentication. +type ErrTwoFactorNotEnrolled struct { + UID int64 +} + +// IsErrTwoFactorNotEnrolled checks if an error is a ErrTwoFactorNotEnrolled. +func IsErrTwoFactorNotEnrolled(err error) bool { + _, ok := err.(ErrTwoFactorNotEnrolled) + return ok +} + +func (err ErrTwoFactorNotEnrolled) Error() string { + return fmt.Sprintf("user not enrolled in 2FA [uid: %d]", err.UID) +} + // ____ ___ .__ .___ // | | \______ | | _________ __| _/ // | | /\____ \| | / _ \__ \ / __ | diff --git a/models/models.go b/models/models.go index 131fd4fdb2..d9716e79bd 100644 --- a/models/models.go +++ b/models/models.go @@ -105,6 +105,7 @@ func init() { new(Notification), new(IssueUser), new(LFSMetaObject), + new(TwoFactor), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/twofactor.go b/models/twofactor.go new file mode 100644 index 0000000000..4c0d3702db --- /dev/null +++ b/models/twofactor.go @@ -0,0 +1,141 @@ +// Copyright 2017 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 models + +import ( + "crypto/md5" + "crypto/subtle" + "encoding/base64" + "time" + + "github.com/Unknwon/com" + "github.com/go-xorm/xorm" + "github.com/pquerna/otp/totp" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/setting" +) + +// TwoFactor represents a two-factor authentication token. +type TwoFactor struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"UNIQUE"` + Secret string + ScratchToken string + + Created time.Time `xorm:"-"` + CreatedUnix int64 `xorm:"INDEX"` + Updated time.Time `xorm:"-"` // Note: Updated must below Created for AfterSet. + UpdatedUnix int64 `xorm:"INDEX"` +} + +// BeforeInsert will be invoked by XORM before inserting a record representing this object. +func (t *TwoFactor) BeforeInsert() { + t.CreatedUnix = time.Now().Unix() +} + +// BeforeUpdate is invoked from XORM before updating this object. +func (t *TwoFactor) BeforeUpdate() { + t.UpdatedUnix = time.Now().Unix() +} + +// AfterSet is invoked from XORM after setting the value of a field of this object. +func (t *TwoFactor) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "created_unix": + t.Created = time.Unix(t.CreatedUnix, 0).Local() + case "updated_unix": + t.Updated = time.Unix(t.UpdatedUnix, 0).Local() + } +} + +// GenerateScratchToken recreates the scratch token the user is using. +func (t *TwoFactor) GenerateScratchToken() error { + token, err := base.GetRandomString(8) + if err != nil { + return err + } + t.ScratchToken = token + return nil +} + +// VerifyScratchToken verifies if the specified scratch token is valid. +func (t *TwoFactor) VerifyScratchToken(token string) bool { + if len(token) == 0 { + return false + } + return subtle.ConstantTimeCompare([]byte(token), []byte(t.ScratchToken)) == 1 +} + +func (t *TwoFactor) getEncryptionKey() []byte { + k := md5.Sum([]byte(setting.SecretKey)) + return k[:] +} + +// SetSecret sets the 2FA secret. +func (t *TwoFactor) SetSecret(secret string) error { + secretBytes, err := com.AESEncrypt(t.getEncryptionKey(), []byte(secret)) + if err != nil { + return err + } + t.Secret = base64.StdEncoding.EncodeToString(secretBytes) + return nil +} + +// ValidateTOTP validates the provided passcode. +func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) { + decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret) + if err != nil { + return false, err + } + secret, err := com.AESDecrypt(t.getEncryptionKey(), decodedStoredSecret) + if err != nil { + return false, err + } + secretStr := string(secret) + return totp.Validate(passcode, secretStr), nil +} + +// NewTwoFactor creates a new two-factor authentication token. +func NewTwoFactor(t *TwoFactor) error { + err := t.GenerateScratchToken() + if err != nil { + return err + } + _, err = x.Insert(t) + return err +} + +// UpdateTwoFactor updates a two-factor authentication token. +func UpdateTwoFactor(t *TwoFactor) error { + _, err := x.Id(t.ID).AllCols().Update(t) + return err +} + +// GetTwoFactorByUID returns the two-factor authentication token associated with +// the user, if any. +func GetTwoFactorByUID(uid int64) (*TwoFactor, error) { + twofa := &TwoFactor{UID: uid} + has, err := x.Get(twofa) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTwoFactorNotEnrolled{uid} + } + return twofa, nil +} + +// DeleteTwoFactorByID deletes two-factor authentication token by given ID. +func DeleteTwoFactorByID(id, userID int64) error { + cnt, err := x.Id(id).Delete(&TwoFactor{ + UID: userID, + }) + if err != nil { + return err + } else if cnt != 1 { + return ErrTwoFactorNotEnrolled{userID} + } + return nil +} |