220 lines
6.2 KiB
Go
220 lines
6.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"math/big"
|
|
"maxwarden/constants"
|
|
"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(constants.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(constants.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: uuid.New().String()
|
|
// 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.EncryptData(&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) < constants.PASSWORD_MIN_LENGTH {
|
|
return errors.New("Password must be at least " + strconv.Itoa(constants.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 < constants.PASSWORD_REQUIRED_UPPERCASE {
|
|
return errors.New("Password must contain at least " + strconv.Itoa(constants.PASSWORD_REQUIRED_UPPERCASE) + " uppercase character(s).")
|
|
}
|
|
|
|
if lowercaseCount < constants.PASSWORD_REQUIRED_LOWERCASE {
|
|
return errors.New("Password must contain at least " + strconv.Itoa(constants.PASSWORD_REQUIRED_LOWERCASE) + " lowercase character(s).")
|
|
}
|
|
|
|
if numberCount < constants.PASSWORD_REQUIRED_NUMBERS {
|
|
return errors.New("Password must contain at least " + strconv.Itoa(constants.PASSWORD_REQUIRED_NUMBERS) + " number(s).")
|
|
}
|
|
|
|
if symbolCount < constants.PASSWORD_REQUIRED_SYMBOLS {
|
|
return errors.New("Password must contain at least " + strconv.Itoa(constants.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
|
|
// }
|