init - add project files
This commit is contained in:
219
auth/auth.go
Normal file
219
auth/auth.go
Normal file
@@ -0,0 +1,219 @@
|
||||
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
|
||||
// }
|
||||
Reference in New Issue
Block a user