first commit
This commit is contained in:
commit
ebafb6ce5a
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module code.wmdillon.com/wmdillon/simpleauth
|
||||
|
||||
go 1.25.5
|
||||
86
password.go
Normal file
86
password.go
Normal file
@ -0,0 +1,86 @@
|
||||
package simpleauth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
var (
|
||||
// All password storage functions are set here to allow a user to write their
|
||||
// own implementation if required.
|
||||
SaltAndHashPasswordString func(password []byte) (salt, hash []byte) = DefaultSaltAndHashPasswordString
|
||||
SaltPasswordString func(password, salt []byte) []byte = DefaultSaltPasswordString
|
||||
HashSaltedPasswordString func(password []byte) []byte = DefaultHashSaltedPasswordString
|
||||
|
||||
NumberChars = "0123456789"
|
||||
SpecialChars = "~`!@#$%^&*()_-+={}[]|\\:;\"'<>,./?"
|
||||
LowercaseChars = "abcdefghijklmnopqrstuvwxyz"
|
||||
UppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
MinPasswordLength = 8
|
||||
MaxPasswordLength = 256
|
||||
|
||||
PasswordIsComplexEnough func([]byte) error = DefaultPasswordIsComplexEnough
|
||||
|
||||
ErrInvalidPassword = errors.New("invalid password")
|
||||
)
|
||||
|
||||
type Password struct {
|
||||
Hash []byte
|
||||
Salt []byte
|
||||
}
|
||||
|
||||
func (p Password) String() string {
|
||||
return "cowardly refusal to print password hash/salt to stdout"
|
||||
}
|
||||
|
||||
func (p Password) Matches(password []byte) bool {
|
||||
return bytes.Equal(p.Hash, HashSaltedPasswordString(SaltPasswordString(password, p.Salt)))
|
||||
}
|
||||
|
||||
func NewPassword(password []byte) (*Password, error) {
|
||||
if err := PasswordIsComplexEnough(password); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrInvalidPassword, err)
|
||||
}
|
||||
salt, hash := SaltAndHashPasswordString(password)
|
||||
return &Password{Hash: hash, Salt: salt}, nil
|
||||
}
|
||||
|
||||
func DefaultHashSaltedPasswordString(saltedPassword []byte) []byte {
|
||||
return []byte(fmt.Sprintf("%064x", sha256.Sum256([]byte(saltedPassword))))
|
||||
}
|
||||
|
||||
func DefaultSaltPasswordString(password, salt []byte) []byte {
|
||||
return []byte(fmt.Sprintf("%s:%s", salt, password))
|
||||
}
|
||||
|
||||
func DefaultSaltAndHashPasswordString(password []byte) (salt, hash []byte) {
|
||||
salt = []byte(fmt.Sprintf("%016x", rand.Uint64()))
|
||||
hash = HashSaltedPasswordString(SaltPasswordString(password, salt))
|
||||
return salt, hash
|
||||
}
|
||||
|
||||
func DefaultPasswordIsComplexEnough(password []byte) error {
|
||||
errs := make([]error, 0, 5)
|
||||
if len := len(password); len < MinPasswordLength {
|
||||
errs = append(errs, fmt.Errorf("requires >= %d chars (got %d)", MinPasswordLength, len))
|
||||
} else if len > MaxPasswordLength {
|
||||
errs = append(errs, fmt.Errorf("requires <= %d chars (got %d)", MaxPasswordLength, len))
|
||||
}
|
||||
if !bytes.ContainsAny(password, LowercaseChars) {
|
||||
errs = append(errs, fmt.Errorf("requires 1+ lowercase char (%s)", LowercaseChars))
|
||||
}
|
||||
if !bytes.ContainsAny(password, UppercaseChars) {
|
||||
errs = append(errs, fmt.Errorf("requires 1+ uppercase char (%s)", UppercaseChars))
|
||||
}
|
||||
if !bytes.ContainsAny(password, NumberChars) {
|
||||
errs = append(errs, fmt.Errorf("requires 1+ number (%s)", NumberChars))
|
||||
}
|
||||
if !bytes.ContainsAny(password, SpecialChars) {
|
||||
errs = append(errs, fmt.Errorf("requires 1+ special char (%s)", SpecialChars))
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
68
password_test.go
Normal file
68
password_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package simpleauth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewPassword(t *testing.T) {
|
||||
ThePassword := "ASimplePassword*23"
|
||||
password, err := NewPassword([]byte(ThePassword))
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v\n", err)
|
||||
}
|
||||
if !password.Matches([]byte(ThePassword)) {
|
||||
t.Fatalf("error: '%s' != '%+v'\n", ThePassword, password)
|
||||
}
|
||||
|
||||
ThePassword = "notcomplexenough"
|
||||
password, err = NewPassword([]byte(ThePassword))
|
||||
if err == nil {
|
||||
t.Fatalf("got nil error...\n")
|
||||
} else if password != nil {
|
||||
t.Fatalf("wanted nil Password; got %s %s\n", password.Hash, password.Salt)
|
||||
}
|
||||
|
||||
ThePassword = "NOTCOMPLEXENOUGH"
|
||||
password, err = NewPassword([]byte(ThePassword))
|
||||
if err == nil {
|
||||
t.Fatalf("got nil error...\n")
|
||||
} else if password != nil {
|
||||
t.Fatalf("wanted nil Password; got %s %s\n", password.Hash, password.Salt)
|
||||
}
|
||||
|
||||
ThePassword = ""
|
||||
password, err = NewPassword([]byte(ThePassword))
|
||||
if err == nil {
|
||||
t.Fatalf("got nil error...\n")
|
||||
} else if password != nil {
|
||||
t.Fatalf("wanted nil Password; got %s %s\n", password.Hash, password.Salt)
|
||||
}
|
||||
|
||||
ThePassword = strings.Repeat("hello", 256)
|
||||
password, err = NewPassword([]byte(ThePassword))
|
||||
if err == nil {
|
||||
t.Fatalf("got nil error...\n")
|
||||
} else if password != nil {
|
||||
t.Fatalf("wanted nil Password; got %s %s\n", password.Hash, password.Salt)
|
||||
}
|
||||
|
||||
ThePassword = "*sdbksldKdf"
|
||||
password, err = NewPassword([]byte(ThePassword))
|
||||
if err == nil {
|
||||
t.Fatalf("got nil error...\n")
|
||||
} else if password != nil {
|
||||
t.Fatalf("wanted nil Password; got %s %s\n", password.Hash, password.Salt)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPasswordStringMethod(t *testing.T) {
|
||||
password, err := NewPassword([]byte("*123ABcd"))
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v\n", err)
|
||||
}
|
||||
if got := password.String(); !strings.Contains(got, "cowardly refusal") {
|
||||
t.Fatalf("error: got '%s' instead of 'cowardly refusal' message\n", got)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user