code.gitea.io/gitea@v1.21.7/models/auth/twofactor.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package auth
     5  
     6  import (
     7  	"context"
     8  	"crypto/md5"
     9  	"crypto/subtle"
    10  	"encoding/base32"
    11  	"encoding/base64"
    12  	"encoding/hex"
    13  	"fmt"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	"code.gitea.io/gitea/modules/secret"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	"code.gitea.io/gitea/modules/timeutil"
    19  	"code.gitea.io/gitea/modules/util"
    20  
    21  	"github.com/minio/sha256-simd"
    22  	"github.com/pquerna/otp/totp"
    23  	"golang.org/x/crypto/pbkdf2"
    24  )
    25  
    26  //
    27  // Two-factor authentication
    28  //
    29  
    30  // ErrTwoFactorNotEnrolled indicates that a user is not enrolled in two-factor authentication.
    31  type ErrTwoFactorNotEnrolled struct {
    32  	UID int64
    33  }
    34  
    35  // IsErrTwoFactorNotEnrolled checks if an error is a ErrTwoFactorNotEnrolled.
    36  func IsErrTwoFactorNotEnrolled(err error) bool {
    37  	_, ok := err.(ErrTwoFactorNotEnrolled)
    38  	return ok
    39  }
    40  
    41  func (err ErrTwoFactorNotEnrolled) Error() string {
    42  	return fmt.Sprintf("user not enrolled in 2FA [uid: %d]", err.UID)
    43  }
    44  
    45  // Unwrap unwraps this as a ErrNotExist err
    46  func (err ErrTwoFactorNotEnrolled) Unwrap() error {
    47  	return util.ErrNotExist
    48  }
    49  
    50  // TwoFactor represents a two-factor authentication token.
    51  type TwoFactor struct {
    52  	ID               int64 `xorm:"pk autoincr"`
    53  	UID              int64 `xorm:"UNIQUE"`
    54  	Secret           string
    55  	ScratchSalt      string
    56  	ScratchHash      string
    57  	LastUsedPasscode string             `xorm:"VARCHAR(10)"`
    58  	CreatedUnix      timeutil.TimeStamp `xorm:"INDEX created"`
    59  	UpdatedUnix      timeutil.TimeStamp `xorm:"INDEX updated"`
    60  }
    61  
    62  func init() {
    63  	db.RegisterModel(new(TwoFactor))
    64  }
    65  
    66  // GenerateScratchToken recreates the scratch token the user is using.
    67  func (t *TwoFactor) GenerateScratchToken() (string, error) {
    68  	tokenBytes, err := util.CryptoRandomBytes(6)
    69  	if err != nil {
    70  		return "", err
    71  	}
    72  	// these chars are specially chosen, avoid ambiguous chars like `0`, `O`, `1`, `I`.
    73  	const base32Chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
    74  	token := base32.NewEncoding(base32Chars).WithPadding(base32.NoPadding).EncodeToString(tokenBytes)
    75  	t.ScratchSalt, _ = util.CryptoRandomString(10)
    76  	t.ScratchHash = HashToken(token, t.ScratchSalt)
    77  	return token, nil
    78  }
    79  
    80  // HashToken return the hashable salt
    81  func HashToken(token, salt string) string {
    82  	tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New)
    83  	return hex.EncodeToString(tempHash)
    84  }
    85  
    86  // VerifyScratchToken verifies if the specified scratch token is valid.
    87  func (t *TwoFactor) VerifyScratchToken(token string) bool {
    88  	if len(token) == 0 {
    89  		return false
    90  	}
    91  	tempHash := HashToken(token, t.ScratchSalt)
    92  	return subtle.ConstantTimeCompare([]byte(t.ScratchHash), []byte(tempHash)) == 1
    93  }
    94  
    95  func (t *TwoFactor) getEncryptionKey() []byte {
    96  	k := md5.Sum([]byte(setting.SecretKey))
    97  	return k[:]
    98  }
    99  
   100  // SetSecret sets the 2FA secret.
   101  func (t *TwoFactor) SetSecret(secretString string) error {
   102  	secretBytes, err := secret.AesEncrypt(t.getEncryptionKey(), []byte(secretString))
   103  	if err != nil {
   104  		return err
   105  	}
   106  	t.Secret = base64.StdEncoding.EncodeToString(secretBytes)
   107  	return nil
   108  }
   109  
   110  // ValidateTOTP validates the provided passcode.
   111  func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
   112  	decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret)
   113  	if err != nil {
   114  		return false, err
   115  	}
   116  	secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
   117  	if err != nil {
   118  		return false, err
   119  	}
   120  	secretStr := string(secretBytes)
   121  	return totp.Validate(passcode, secretStr), nil
   122  }
   123  
   124  // NewTwoFactor creates a new two-factor authentication token.
   125  func NewTwoFactor(ctx context.Context, t *TwoFactor) error {
   126  	_, err := db.GetEngine(ctx).Insert(t)
   127  	return err
   128  }
   129  
   130  // UpdateTwoFactor updates a two-factor authentication token.
   131  func UpdateTwoFactor(ctx context.Context, t *TwoFactor) error {
   132  	_, err := db.GetEngine(ctx).ID(t.ID).AllCols().Update(t)
   133  	return err
   134  }
   135  
   136  // GetTwoFactorByUID returns the two-factor authentication token associated with
   137  // the user, if any.
   138  func GetTwoFactorByUID(ctx context.Context, uid int64) (*TwoFactor, error) {
   139  	twofa := &TwoFactor{}
   140  	has, err := db.GetEngine(ctx).Where("uid=?", uid).Get(twofa)
   141  	if err != nil {
   142  		return nil, err
   143  	} else if !has {
   144  		return nil, ErrTwoFactorNotEnrolled{uid}
   145  	}
   146  	return twofa, nil
   147  }
   148  
   149  // HasTwoFactorByUID returns the two-factor authentication token associated with
   150  // the user, if any.
   151  func HasTwoFactorByUID(ctx context.Context, uid int64) (bool, error) {
   152  	return db.GetEngine(ctx).Where("uid=?", uid).Exist(&TwoFactor{})
   153  }
   154  
   155  // DeleteTwoFactorByID deletes two-factor authentication token by given ID.
   156  func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error {
   157  	cnt, err := db.GetEngine(ctx).ID(id).Delete(&TwoFactor{
   158  		UID: userID,
   159  	})
   160  	if err != nil {
   161  		return err
   162  	} else if cnt != 1 {
   163  		return ErrTwoFactorNotEnrolled{userID}
   164  	}
   165  	return nil
   166  }