mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-06 01:07:16 +08:00
350 lines
7.4 KiB
Go
350 lines
7.4 KiB
Go
package entity
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
"time"
|
|
|
|
"github.com/pquerna/otp"
|
|
"github.com/pquerna/otp/totp"
|
|
|
|
"github.com/photoprism/photoprism/pkg/authn"
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
|
)
|
|
|
|
// Passcode represents a two-factor authentication key.
|
|
type Passcode struct {
|
|
UID string `gorm:"type:VARBINARY(255);primary_key;" json:"UID"`
|
|
KeyType string `gorm:"size:64;default:'';primary_key;" json:"Type" yaml:"Type"`
|
|
KeyURL string `gorm:"size:2048;default:'';column:key_url;" json:"-" yaml:"-"`
|
|
key *otp.Key `gorm:"-" yaml:"-"`
|
|
RecoveryCode string `gorm:"size:255;default:'';" json:"-" yaml:"-"`
|
|
VerifiedAt *time.Time `json:"VerifiedAt" yaml:"-"`
|
|
ActivatedAt *time.Time `json:"ActivatedAt" yaml:"-"`
|
|
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
|
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
|
}
|
|
|
|
// TableName returns the entity table name.
|
|
func (Passcode) TableName() string {
|
|
return "passcodes"
|
|
}
|
|
|
|
// NewPasscode returns a new two-factor authentication key or nil if no valid entity UID was provided.
|
|
func NewPasscode(uid string, keyUrl, recoveryCode string) (*Passcode, error) {
|
|
// Create new authentication key.
|
|
m := &Passcode{
|
|
UID: uid,
|
|
KeyURL: keyUrl,
|
|
RecoveryCode: clean.Token(recoveryCode),
|
|
CreatedAt: Now(),
|
|
UpdatedAt: Now(),
|
|
VerifiedAt: nil,
|
|
ActivatedAt: nil,
|
|
}
|
|
|
|
// Return an error if the uid or key are invalid.
|
|
if rnd.InvalidUID(uid, 0) {
|
|
return m, errors.New("auth: invalid uid")
|
|
} else if keyUrl == "" {
|
|
return m, errors.New("auth: invalid url")
|
|
} else if err := m.SetKeyURL(keyUrl); err != nil {
|
|
return m, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// FindPasscode returns the matching key or nil if it was not found.
|
|
func FindPasscode(find Passcode) *Passcode {
|
|
m := &Passcode{}
|
|
|
|
keyType := authn.Key(find.KeyType)
|
|
|
|
// Build query.
|
|
stmt := UnscopedDb()
|
|
if rnd.IsUID(find.UID, 0) {
|
|
stmt = stmt.Where("uid = ? AND key_type = ?", find.UID, keyType.String())
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
// Find matching record.
|
|
if err := stmt.First(m).Error; err != nil {
|
|
return nil
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// Create new entity in the database.
|
|
func (m *Passcode) Create() (err error) {
|
|
return Db().Create(m).Error
|
|
}
|
|
|
|
// Save updates the record in the database or inserts a new record if it does not exist yet.
|
|
func (m *Passcode) Save() (err error) {
|
|
return UnscopedDb().Save(m).Error
|
|
}
|
|
|
|
// Delete deletes the entity record.
|
|
func (m *Passcode) Delete() (err error) {
|
|
if m == nil {
|
|
return fmt.Errorf("entity is nil")
|
|
} else if m.UID == "" {
|
|
return fmt.Errorf("uid not set")
|
|
}
|
|
|
|
err = UnscopedDb().Delete(m).Error
|
|
|
|
return err
|
|
}
|
|
|
|
// Updates multiple properties in the database.
|
|
func (m *Passcode) Updates(values interface{}) error {
|
|
return UnscopedDb().Model(m).Updates(values).Error
|
|
}
|
|
|
|
// SetUID assigns a valid entity UID.
|
|
func (m *Passcode) SetUID(uid string) *Passcode {
|
|
if rnd.IsUID(uid, 0) {
|
|
m.UID = uid
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// InvalidUID checks if the entity UID is invalid.
|
|
func (m *Passcode) InvalidUID() bool {
|
|
if m == nil {
|
|
return true
|
|
}
|
|
|
|
return !rnd.IsUID(m.UID, 0)
|
|
}
|
|
|
|
// Key returns the parsed two-factor authentication key or nil if the KeyURL is invalid.
|
|
func (m *Passcode) Key() *otp.Key {
|
|
if m == nil {
|
|
return nil
|
|
} else if m.key != nil {
|
|
return m.key
|
|
}
|
|
|
|
key, err := otp.NewKeyFromURL(m.KeyURL)
|
|
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
m.key = key
|
|
|
|
return m.key
|
|
}
|
|
|
|
// SetKey sets a new two-factor authentication key.
|
|
func (m *Passcode) SetKey(key *otp.Key) error {
|
|
if key == nil {
|
|
return errors.New("auth: key is nil")
|
|
}
|
|
|
|
if keyType := key.Type(); authn.KeyTOTP.NotEqual(keyType) {
|
|
return fmt.Errorf("auth: invalid key type %s", clean.Log(keyType))
|
|
} else if key.Secret() == "" {
|
|
return errors.New("auth: invalid key secret")
|
|
}
|
|
|
|
m.KeyType = key.Type()
|
|
m.KeyURL = key.URL()
|
|
m.key = key
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetKeyURL sets a new two-factor authentication key based on the URL provided.
|
|
func (m *Passcode) SetKeyURL(keyUrl string) error {
|
|
key, err := otp.NewKeyFromURL(keyUrl)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("auth: %s", err)
|
|
} else if key == nil {
|
|
return errors.New("auth: failed to parse url")
|
|
}
|
|
|
|
return m.SetKey(key)
|
|
}
|
|
|
|
// Secret returns the key secret or an empty string if none is set.
|
|
func (m *Passcode) Secret() string {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
|
|
key := m.Key()
|
|
|
|
if key == nil {
|
|
return ""
|
|
}
|
|
|
|
return key.Secret()
|
|
}
|
|
|
|
// Type returns the normalized key type.
|
|
func (m *Passcode) Type() authn.KeyType {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
|
|
return authn.Key(m.KeyType)
|
|
}
|
|
|
|
// GenerateCode returns a valid passcode for testing.
|
|
func (m *Passcode) GenerateCode() (code string, err error) {
|
|
if m == nil {
|
|
return "", errors.New("passcode is nil")
|
|
}
|
|
|
|
// Get authentication key.
|
|
key := m.Key()
|
|
|
|
if key == nil {
|
|
return "", authn.ErrInvalidPasscodeKey
|
|
}
|
|
|
|
// Generate code depending on key type.
|
|
switch m.Type() {
|
|
case authn.KeyTOTP:
|
|
code, err = totp.GenerateCodeCustom(
|
|
key.Secret(),
|
|
time.Now().UTC(),
|
|
totp.ValidateOpts{
|
|
Period: uint(key.Period()),
|
|
Skew: 1,
|
|
Digits: key.Digits(),
|
|
Algorithm: key.Algorithm(),
|
|
},
|
|
)
|
|
default:
|
|
return "", authn.ErrInvalidPasscodeType
|
|
}
|
|
|
|
// Return result.
|
|
return code, err
|
|
}
|
|
|
|
// Valid checks if the passcode provided is valid.
|
|
func (m *Passcode) Valid(code string) (valid bool, recovery bool, err error) {
|
|
// Validate arguments.
|
|
if m == nil {
|
|
return false, false, errors.New("passcode is nil")
|
|
} else if code == "" {
|
|
return false, false, authn.ErrPasscodeRequired
|
|
} else if len(code) > 255 {
|
|
return false, false, authn.ErrInvalidPasscodeFormat
|
|
}
|
|
|
|
// Get authentication key.
|
|
key := m.Key()
|
|
|
|
if key == nil {
|
|
return false, false, authn.ErrInvalidPasscodeKey
|
|
}
|
|
|
|
// Check if recovery code has been used.
|
|
if m.RecoveryCode == code {
|
|
return true, true, nil
|
|
}
|
|
|
|
// Verify passcode.
|
|
switch m.Type() {
|
|
case authn.KeyTOTP:
|
|
valid, err = totp.ValidateCustom(
|
|
code,
|
|
key.Secret(),
|
|
time.Now().UTC(),
|
|
totp.ValidateOpts{
|
|
Period: uint(key.Period()),
|
|
Skew: 1,
|
|
Digits: key.Digits(),
|
|
Algorithm: key.Algorithm(),
|
|
},
|
|
)
|
|
default:
|
|
return false, false, authn.ErrInvalidPasscodeType
|
|
}
|
|
|
|
// Check if an error has been returned.
|
|
if err != nil {
|
|
return valid, false, err
|
|
}
|
|
|
|
// Set verified timestamp if nil.
|
|
if valid && m.VerifiedAt == nil {
|
|
m.VerifiedAt = TimeStamp()
|
|
err = m.Updates(Map{"VerifiedAt": m.VerifiedAt})
|
|
}
|
|
|
|
// Return result.
|
|
return valid, false, err
|
|
}
|
|
|
|
// Activate activates the passcode.
|
|
func (m *Passcode) Activate() (err error) {
|
|
if m == nil {
|
|
return errors.New("passcode is nil")
|
|
}
|
|
|
|
if m.VerifiedAt == nil {
|
|
return authn.ErrPasscodeNotVerified
|
|
} else if m.ActivatedAt != nil {
|
|
return authn.ErrPasscodeAlreadyActivated
|
|
} else {
|
|
m.ActivatedAt = TimeStamp()
|
|
err = m.Updates(Map{"ActivatedAt": m.ActivatedAt})
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Image returns an image with a QR Code that can be used to initialize compatible authenticator apps.
|
|
func (m *Passcode) Image(size int) (image.Image, error) {
|
|
if m == nil {
|
|
return nil, errors.New("key is nil")
|
|
}
|
|
|
|
key := m.Key()
|
|
|
|
if key == nil {
|
|
return nil, authn.ErrPasscodeNotSetUp
|
|
}
|
|
|
|
return key.Image(size, size)
|
|
}
|
|
|
|
// Png returns a PNG image buffer with a QR Code that can be used to initialize compatible authenticator apps.
|
|
func (m *Passcode) Png(size int) *bytes.Buffer {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
img, err := m.Image(size)
|
|
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
err = png.Encode(&buf, img)
|
|
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &buf
|
|
}
|