photoprism/internal/entity/password.go
Michael Mayer 096b306420 Auth: Reduce bcrypt cost for faster login on small devices #3718
Signed-off-by: Michael Mayer <michael@photoprism.app>
2023-09-12 09:36:23 +02:00

136 lines
3.3 KiB
Go

package entity
import (
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
// DefaultPasswordCost specifies the cost of the BCrypt Password Hash,
// see https://github.com/photoprism/photoprism/issues/3718.
var DefaultPasswordCost = 12
// Password represents a password hash.
type Password struct {
UID string `gorm:"type:VARBINARY(255);primary_key;" json:"UID"`
Hash string `deepcopier:"skip" gorm:"type:VARBINARY(255);" json:"Hash"`
CreatedAt time.Time `deepcopier:"skip" json:"CreatedAt"`
UpdatedAt time.Time `deepcopier:"skip" json:"UpdatedAt"`
}
// TableName returns the entity table name.
func (Password) TableName() string {
return "passwords"
}
// NewPassword creates a new password instance.
func NewPassword(uid, pw string, allowHash bool) Password {
if uid == "" {
panic("auth: cannot set password without uid")
}
m := Password{UID: uid}
if pw != "" {
if err := m.SetPassword(pw, allowHash); err != nil {
log.Errorf("auth: %s", err)
}
}
return m
}
// SetPassword sets a new password stored as hash.
func (m *Password) SetPassword(pw string, allowHash bool) error {
// Remove leading and trailing white space.
pw = clean.Password(pw)
// Check if password is too short or too long.
if len([]rune(pw)) < 1 {
return fmt.Errorf("password is too short")
} else if len(pw) > txt.ClipPassword {
return fmt.Errorf("password must have less than %d characters", txt.ClipPassword)
}
// Check if string already is a bcrypt hash.
if allowHash {
if cost, err := bcrypt.Cost([]byte(pw)); err == nil && cost >= bcrypt.MinCost {
m.Hash = pw
return nil
}
}
// Generate hash from plain text string using the default password cost.
if bytes, err := bcrypt.GenerateFromPassword([]byte(pw), DefaultPasswordCost); err != nil {
return err
} else {
m.Hash = string(bytes)
return nil
}
}
// IsValid checks if the password is correct.
func (m *Password) IsValid(s string) bool {
return !m.IsWrong(s)
}
// IsWrong checks if the specified password is incorrect.
func (m *Password) IsWrong(s string) bool {
if m.IsEmpty() {
// No password set.
return true
} else if s = clean.Password(s); s == "" {
// No password provided.
return true
} else if err := bcrypt.CompareHashAndPassword([]byte(m.Hash), []byte(s)); err != nil {
// Wrong password.
return true
}
// Ok.
return false
}
// Create inserts a new row to the database.
func (m *Password) Create() error {
return Db().Create(m).Error
}
// Save updates the record in the database or inserts a new record if it does not already exist.
func (m *Password) Save() error {
return Db().Save(m).Error
}
// FindPassword returns an entity pointer if exists.
func FindPassword(uid string) *Password {
result := Password{}
if err := Db().Where("uid = ?", uid).First(&result).Error; err == nil {
return &result
}
return nil
}
// Cost returns the hashing cost of the currently set password.
func (m *Password) Cost() (int, error) {
if m.IsEmpty() {
return 0, fmt.Errorf("password is empty")
}
return bcrypt.Cost([]byte(m.Hash))
}
// IsEmpty returns true if no password is set.
func (m *Password) IsEmpty() bool {
return m.Hash == ""
}
// String returns the BCrypt Password Hash.
func (m *Password) String() string {
return m.Hash
}