Files
maxwarden/auth/auth.go
2025-03-06 23:54:11 -05:00

220 lines
6.2 KiB
Go

package auth
import (
"crypto/rand"
"errors"
"math/big"
"maxwarden/config"
"maxwarden/entries"
"maxwarden/security"
"maxwarden/users"
"strconv"
"strings"
"time"
"unicode"
)
const (
UNAUTHORIZED_MESSAGE string = "Unauthorized access"
)
type Identity struct {
UserID int32
SecurityStamp string
MasterKey string
Permissions users.Permissions
Authenticated bool
Expiration time.Time
}
func NewIdentity(userid int32, securityStamp string, masterKey string, rememberMe bool) *Identity {
expirationDuration := time.Duration(time.Hour * 24 * time.Duration(config.IDENTITY_COOKIE_EXPIRY_DAYS))
expiration := time.Now().Add(expirationDuration)
// hash password input
mk := security.SHA512_58(masterKey)
return &Identity{
UserID: userid,
SecurityStamp: securityStamp,
Authenticated: true,
MasterKey: mk,
Expiration: expiration,
}
}
func Authenticate(username string, password string) (int32, string, bool) {
// time attack partial mitigation
// adds up to 0.5 seconds to the response time
// this technically does not prevent a time attack, since there is still time variance without the randomness added.
// you could theoretically take an average of a 'valid user; incorrect password' vs 'invalid user' response times
// to figure out if a user exists, but you would need a lot of data to do that.
// this should make it *extremely* unlikely to do when paired with 'n login attempt per ip/minute/fingerprint'
// since you would need way more than `n` login attempts to collect an accurate average
// https://security.stackexchange.com/questions/96489/can-i-prevent-timing-attacks-with-random-delays/96493#96493
// https://www.reddit.com/r/PHP/comments/kn6ezp/have_you_secured_your_signup_process_against_a/
randomSeconds, _ := rand.Int(rand.Reader, big.NewInt(500))
randomDuration := time.Duration(randomSeconds.Int64()) * time.Millisecond
time.Sleep(randomDuration)
user, userErr := users.FetchByUsername(username)
if userErr != nil || user.FailedAttempts > int32(config.MAX_LOGIN_ATTEMPTS) {
// set user password to dummy password to keep timing consistent when validating password
user.Password = "$2a$14$KW5OO1wZqGGq3SrpBFj0Oema5DG8Ph7lZJvq0ECkkYBpNFom6b9vO"
security.ComparePasswords(password, user.Password)
return 0, "", false
}
result := security.ComparePasswords(password, user.Password)
if !result {
user.FailedAttempts += 1
users.Update(user)
} else {
user.FailedAttempts = 0
// seed data
if user.Data == nil || len(user.Data) == 0 {
secrets := []entries.Secret{}
// THE FUNNY THING
// for range 2500000 {
// secrets = append(secrets, entries.Secret{
// ID: security.RandBase58String(32),
// Description: "some website",
// URL: "https://example.com",
// Notes: "test notes something here i like writing notes lalalalala test test",
// Username: "username2345",
// Password: "laksjdflkjasdlfkj2934829384sldkfj",
// })
// }
mk := security.SHA512_58(password)
user.Data, _ = security.EncryptDataWithKey(&secrets, mk)
}
users.Update(user)
}
return user.ID, user.SecurityStamp, result
}
func CheckPasswordCriteria(password string) error {
if strings.TrimSpace(password) == "" {
return errors.New("Password cannot be blank.")
}
if len(password) < config.PASSWORD_MIN_LENGTH {
return errors.New("Password must be at least " + strconv.Itoa(config.PASSWORD_MIN_LENGTH) + " characters long.")
}
uppercaseCount := 0
lowercaseCount := 0
numberCount := 0
symbolCount := 0
for _, r := range password {
if unicode.IsUpper(r) {
uppercaseCount += 1
}
if unicode.IsLower(r) {
lowercaseCount += 1
}
if unicode.IsNumber(r) {
numberCount += 1
}
if !unicode.IsNumber(r) && !unicode.IsLower(r) && !unicode.IsUpper(r) {
symbolCount += 1
}
}
if uppercaseCount < config.PASSWORD_REQUIRED_UPPERCASE {
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_UPPERCASE) + " uppercase character(s).")
}
if lowercaseCount < config.PASSWORD_REQUIRED_LOWERCASE {
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_LOWERCASE) + " lowercase character(s).")
}
if numberCount < config.PASSWORD_REQUIRED_NUMBERS {
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_NUMBERS) + " number(s).")
}
if symbolCount < config.PASSWORD_REQUIRED_SYMBOLS {
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_SYMBOLS) + " symbol(s).")
}
return nil
}
// func ChangePassword(user models.User, oldPassword string, newPassword string, passwordConfirm string, noCheck bool) (models.User, error) {
// // noCheck skips criteria validation, and confirmation validation
// if noCheck {
// if newPassword != passwordConfirm {
// log.Println("Passwords do not match")
// return user, errors.New("passwords do not match")
// }
// critErr := CheckPasswordCriteria(newPassword)
// if critErr != nil {
// return user, critErr
// }
// if !security.ComparePasswords(oldPassword, user.Password) {
// return user, errors.New("old password incorrect")
// }
// }
// newHash, hashErr := security.HashPassword(newPassword)
// if hashErr != nil {
// return user, errors.New("could not hash password")
// }
// user.Password = newHash
// updateErr := database.UpdateUser(user)
// if updateErr != nil {
// return user, errors.New("could not update user")
// }
// return user, nil
// }
// // wrapper with less args for skipping validation, confirmation
// func ChangePasswordNoCheck(user models.User, newPassword string) (models.User, error) {
// return ChangePassword(user, "", newPassword, "", true)
// }
// // hard reset user password without confirmation or record.
// // should only be used for developer purposes
// func ResetPasswordNoConfirm(userid int) (models.User, error) {
// user, err := database.FetchUserById(userid)
// if err != nil {
// return user, err
// }
// hash, hashErr := security.HashPassword(config.GetConfig().IdentityDefaultPassword)
// if hashErr != nil {
// return user, hashErr
// }
// user.Password = hash
// updateErr := database.UpdateUser(user)
// if updateErr != nil {
// return user, updateErr
// }
// return user, nil
// }