|
- // 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/aes"
- "crypto/cipher"
- "crypto/md5"
- "crypto/rand"
- "crypto/sha256"
- "crypto/subtle"
- "encoding/base64"
- "errors"
- "fmt"
- "io"
-
- "code.gitea.io/gitea/modules/generate"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
-
- "github.com/pquerna/otp/totp"
- "golang.org/x/crypto/pbkdf2"
- )
-
- // TwoFactor represents a two-factor authentication token.
- type TwoFactor struct {
- ID int64 `xorm:"pk autoincr"`
- UID int64 `xorm:"UNIQUE"`
- Secret string
- ScratchSalt string
- ScratchHash string
- LastUsedPasscode string `xorm:"VARCHAR(10)"`
- CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
- UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
- }
-
- // GenerateScratchToken recreates the scratch token the user is using.
- func (t *TwoFactor) GenerateScratchToken() (string, error) {
- token, err := generate.GetRandomString(8)
- if err != nil {
- return "", err
- }
- 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.
- func (t *TwoFactor) VerifyScratchToken(token string) bool {
- if len(token) == 0 {
- return false
- }
- tempHash := hashToken(token, t.ScratchSalt)
- return subtle.ConstantTimeCompare([]byte(t.ScratchHash), []byte(tempHash)) == 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 := 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 := aesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
- if err != nil {
- return false, err
- }
- secretStr := string(secret)
- return totp.Validate(passcode, secretStr), nil
- }
-
- // aesEncrypt encrypts text and given key with AES.
- func aesEncrypt(key, text []byte) ([]byte, error) {
- block, err := aes.NewCipher(key)
- if err != nil {
- return nil, err
- }
- b := base64.StdEncoding.EncodeToString(text)
- ciphertext := make([]byte, aes.BlockSize+len(b))
- iv := ciphertext[:aes.BlockSize]
- if _, err := io.ReadFull(rand.Reader, iv); err != nil {
- return nil, err
- }
- cfb := cipher.NewCFBEncrypter(block, iv)
- cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
- return ciphertext, nil
- }
-
- // aesDecrypt decrypts text and given key with AES.
- func aesDecrypt(key, text []byte) ([]byte, error) {
- block, err := aes.NewCipher(key)
- if err != nil {
- return nil, err
- }
- if len(text) < aes.BlockSize {
- return nil, errors.New("ciphertext too short")
- }
- iv := text[:aes.BlockSize]
- text = text[aes.BlockSize:]
- cfb := cipher.NewCFBDecrypter(block, iv)
- cfb.XORKeyStream(text, text)
- data, err := base64.StdEncoding.DecodeString(string(text))
- if err != nil {
- return nil, err
- }
- return data, nil
- }
-
- // NewTwoFactor creates a new two-factor authentication token.
- func NewTwoFactor(t *TwoFactor) error {
- _, 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
- }
|