mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 23:33:15 -05:00
a8c61532d2
- Currently the TOTP secrets are stored using the `secrets` module with as key the MD5 hash of the Secretkey, the `secrets` module uses general bad practices. This patch migrates the secrets to use the `keying` module (#5041) which is easier to use and use better practices to store secrets in databases. - Migration test added. - Remove the Forgejo migration databases, and let the gitea migration databases also run forgejo migration databases. This is required as the Forgejo migration is now also touching tables that the forgejo migration didn't create itself.
159 lines
4.6 KiB
Go
159 lines
4.6 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base32"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/modules/keying"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
"github.com/pquerna/otp/totp"
|
|
"golang.org/x/crypto/pbkdf2"
|
|
)
|
|
|
|
//
|
|
// 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)
|
|
}
|
|
|
|
// Unwrap unwraps this as a ErrNotExist err
|
|
func (err ErrTwoFactorNotEnrolled) Unwrap() error {
|
|
return util.ErrNotExist
|
|
}
|
|
|
|
// TwoFactor represents a two-factor authentication token.
|
|
type TwoFactor struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
UID int64 `xorm:"UNIQUE"`
|
|
Secret []byte `xorm:"BLOB"`
|
|
ScratchSalt string
|
|
ScratchHash string
|
|
LastUsedPasscode string `xorm:"VARCHAR(10)"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(TwoFactor))
|
|
}
|
|
|
|
// GenerateScratchToken recreates the scratch token the user is using.
|
|
func (t *TwoFactor) GenerateScratchToken() (string, error) {
|
|
tokenBytes, err := util.CryptoRandomBytes(6)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// these chars are specially chosen, avoid ambiguous chars like `0`, `O`, `1`, `I`.
|
|
const base32Chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
token := base32.NewEncoding(base32Chars).WithPadding(base32.NoPadding).EncodeToString(tokenBytes)
|
|
t.ScratchSalt, _ = util.CryptoRandomString(10)
|
|
t.ScratchHash = HashToken(token, t.ScratchSalt)
|
|
return token, nil
|
|
}
|
|
|
|
// HashToken return the hashable salt
|
|
func HashToken(token, salt string) string {
|
|
tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New)
|
|
return hex.EncodeToString(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
|
|
}
|
|
|
|
// SetSecret sets the 2FA secret.
|
|
func (t *TwoFactor) SetSecret(secretString string) {
|
|
key := keying.DeriveKey(keying.ContextTOTP)
|
|
t.Secret = key.Encrypt([]byte(secretString), keying.ColumnAndID("secret", t.ID))
|
|
}
|
|
|
|
// ValidateTOTP validates the provided passcode.
|
|
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
|
|
key := keying.DeriveKey(keying.ContextTOTP)
|
|
secret, err := key.Decrypt(t.Secret, keying.ColumnAndID("secret", t.ID))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return totp.Validate(passcode, string(secret)), nil
|
|
}
|
|
|
|
// NewTwoFactor creates a new two-factor authentication token.
|
|
func NewTwoFactor(ctx context.Context, t *TwoFactor, secret string) error {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
sess := db.GetEngine(ctx)
|
|
_, err := sess.Insert(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
t.SetSecret(secret)
|
|
_, err = sess.Cols("secret").ID(t.ID).Update(t)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// UpdateTwoFactor updates a two-factor authentication token.
|
|
func UpdateTwoFactor(ctx context.Context, t *TwoFactor) error {
|
|
_, err := db.GetEngine(ctx).ID(t.ID).AllCols().Update(t)
|
|
return err
|
|
}
|
|
|
|
// GetTwoFactorByUID returns the two-factor authentication token associated with
|
|
// the user, if any.
|
|
func GetTwoFactorByUID(ctx context.Context, uid int64) (*TwoFactor, error) {
|
|
twofa := &TwoFactor{}
|
|
has, err := db.GetEngine(ctx).Where("uid=?", uid).Get(twofa)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrTwoFactorNotEnrolled{uid}
|
|
}
|
|
return twofa, nil
|
|
}
|
|
|
|
// HasTwoFactorByUID returns the two-factor authentication token associated with
|
|
// the user, if any.
|
|
func HasTwoFactorByUID(ctx context.Context, uid int64) (bool, error) {
|
|
return db.GetEngine(ctx).Where("uid=?", uid).Exist(&TwoFactor{})
|
|
}
|
|
|
|
// DeleteTwoFactorByID deletes two-factor authentication token by given ID.
|
|
func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error {
|
|
cnt, err := db.GetEngine(ctx).ID(id).Delete(&TwoFactor{
|
|
UID: userID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
} else if cnt != 1 {
|
|
return ErrTwoFactorNotEnrolled{userID}
|
|
}
|
|
return nil
|
|
}
|