From df1596312d9fb7cec1d065690303f02d06014c30 Mon Sep 17 00:00:00 2001 From: Sergey Chubaryan Date: Fri, 23 Aug 2024 22:59:54 +0300 Subject: [PATCH] improve password validation, add charset generator --- src/charsets/charsets.go | 134 +++++++++++++++++++++++++ src/charsets/enum.go | 36 +++++++ src/core/services/shortlink_service.go | 17 ++-- src/core/utils/password.go | 44 +++++++- src/core/utils/random.go | 69 ------------- 5 files changed, 223 insertions(+), 77 deletions(-) create mode 100644 src/charsets/charsets.go create mode 100644 src/charsets/enum.go delete mode 100644 src/core/utils/random.go diff --git a/src/charsets/charsets.go b/src/charsets/charsets.go new file mode 100644 index 0000000..1632d02 --- /dev/null +++ b/src/charsets/charsets.go @@ -0,0 +1,134 @@ +package charsets + +import "strings" + +type RandInt interface { + Int() int +} + +type Charset interface { + TestRune(char rune) bool + RandomRune(r RandInt) rune + RandomString(r RandInt, size int) string + + String() string +} + +func NewCharsetFromASCII(offset, size int) Charset { + return charsetASCII{offset: offset, size: size} +} + +type charsetASCII struct { + offset int + size int +} + +func (c charsetASCII) TestRune(char rune) bool { + return int(char) >= c.offset && int(char) < c.offset+c.size +} + +func (c charsetASCII) RandomRune(r RandInt) rune { + num := c.offset + r.Int()%(c.size-1) + return rune(num) +} + +func (c charsetASCII) RandomString(r RandInt, size int) string { + builder := strings.Builder{} + for i := 0; i < size; i++ { + builder.WriteRune(c.RandomRune(r)) + } + return builder.String() +} + +func (c charsetASCII) String() string { + builder := strings.Builder{} + for i := 0; i < c.size; i++ { + builder.WriteRune(rune(c.offset + i)) + } + return builder.String() +} + +func NewCharsetFromString(s string) Charset { + charsArray := make([]rune, len(s)) + charsMap := make(map[rune]bool, len(s)) + for i, v := range s { + charsArray[i] = v + charsMap[v] = true + } + + return charsetFromString{ + charsArray: charsArray, + charsMap: charsMap, + } +} + +type charsetFromString struct { + charsMap map[rune]bool + charsArray []rune +} + +func (c charsetFromString) TestRune(char rune) bool { + return c.charsMap[char] +} + +func (c charsetFromString) RandomRune(r RandInt) rune { + num := r.Int() % (len(c.charsArray) - 1) + return c.charsArray[num] +} + +func (c charsetFromString) RandomString(r RandInt, size int) string { + builder := strings.Builder{} + for i := 0; i < size; i++ { + builder.WriteRune(c.RandomRune(r)) + } + return builder.String() +} + +func (c charsetFromString) String() string { + builder := strings.Builder{} + for _, v := range c.charsArray { + builder.WriteRune(v) + } + return builder.String() +} + +func NewCharsetUnion(opts ...Charset) Charset { + charsets := []Charset{} + return charsetUnion{ + charsets: append(charsets, opts...), + } +} + +type charsetUnion struct { + charsets []Charset +} + +func (c charsetUnion) TestRune(char rune) bool { + for _, charset := range c.charsets { + if charset.TestRune(char) { + return true + } + } + return false +} + +func (c charsetUnion) RandomRune(r RandInt) rune { + index := r.Int() % (len(c.charsets) - 1) + charset := c.charsets[index] + + return charset.RandomRune(r) +} + +func (c charsetUnion) RandomString(r RandInt, size int) string { + builder := strings.Builder{} + for i := 0; i < size; i++ { + index := r.Int() % (len(c.charsets) - 1) + charset := c.charsets[index] + builder.WriteRune(charset.RandomRune(r)) + } + return builder.String() +} + +func (c charsetUnion) String() string { + return "" +} diff --git a/src/charsets/enum.go b/src/charsets/enum.go new file mode 100644 index 0000000..42b58d7 --- /dev/null +++ b/src/charsets/enum.go @@ -0,0 +1,36 @@ +package charsets + +type CharsetType int + +const ( + CharsetTypeAll CharsetType = iota + CharsetTypeLettersLower + CharsetTypeLettersUpper + CharsetTypeLetters + CharsetTypeNumeric +) + +var ( + charsetNumeric = NewCharsetFromASCII(0x30, 10) + charsetLettersLower = NewCharsetFromASCII(0x41, 26) + charsetLettersUpper = NewCharsetFromASCII(0x61, 26) + charsetLetters = NewCharsetUnion(charsetLettersLower, charsetLettersUpper) + charsetAll = NewCharsetUnion(charsetNumeric, charsetLettersLower, charsetLettersUpper) +) + +func GetCharset(charsetType CharsetType) Charset { + switch charsetType { + case CharsetTypeNumeric: + return charsetNumeric + case CharsetTypeLettersLower: + return charsetLettersLower + case CharsetTypeLettersUpper: + return charsetLettersLower + case CharsetTypeLetters: + return charsetLetters + case CharsetTypeAll: + return charsetAll + default: + return nil + } +} diff --git a/src/core/services/shortlink_service.go b/src/core/services/shortlink_service.go index 198690a..866d27a 100644 --- a/src/core/services/shortlink_service.go +++ b/src/core/services/shortlink_service.go @@ -1,9 +1,11 @@ package services import ( + "backend/src/charsets" "backend/src/core/repos" - "backend/src/core/utils" "fmt" + "math/rand" + "time" ) type ShortlinkService interface { @@ -18,18 +20,21 @@ type NewShortlinkServiceParams struct { func NewShortlinkSevice(params NewShortlinkServiceParams) ShortlinkService { return &shortlinkService{ - randomUtil: *utils.NewRand(), - cache: params.Cache, + cache: params.Cache, } } type shortlinkService struct { - randomUtil utils.RandomUtil - cache repos.Cache[string, string] + cache repos.Cache[string, string] } func (s *shortlinkService) CreateLink(in string) (string, error) { - str := s.randomUtil.RandomID(10, utils.CharsetAll) + charset := charsets.GetCharset(charsets.CharsetTypeAll) + + src := rand.NewSource(time.Now().UnixMicro()) + randGen := rand.New(src) + str := charset.RandomString(randGen, 10) + s.cache.Set(str, in, 7*24*60*60) return str, nil } diff --git a/src/core/utils/password.go b/src/core/utils/password.go index 930a38c..c237312 100644 --- a/src/core/utils/password.go +++ b/src/core/utils/password.go @@ -1,6 +1,7 @@ package utils import ( + "backend/src/charsets" "fmt" "golang.org/x/crypto/bcrypt" @@ -13,10 +14,15 @@ type PasswordUtil interface { } func NewPasswordUtil() PasswordUtil { - return &passwordUtil{} + specialChars := `!@#$%^&*()_-+={[}]|\:;"'<,>.?/` + return &passwordUtil{ + charsetSpecialChars: charsets.NewCharsetFromString(specialChars), + } } -type passwordUtil struct{} +type passwordUtil struct { + charsetSpecialChars charsets.Charset +} func (b *passwordUtil) Hash(password string) (string, error) { bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -31,5 +37,39 @@ func (b *passwordUtil) Validate(password string) error { if len(password) < 8 { return fmt.Errorf("password must contain 8 or more characters") } + + charsetUpper := charsets.GetCharset(charsets.CharsetTypeLettersUpper) + charsetLower := charsets.GetCharset(charsets.CharsetTypeLettersLower) + + lowercaseLettersCount := 0 + uppercaseLettersCount := 0 + specialCharsCount := 0 + for _, v := range password { + if b.charsetSpecialChars.TestRune(v) { + specialCharsCount++ + continue + } + + if charsetUpper.TestRune(v) { + uppercaseLettersCount++ + continue + } + + if charsetLower.TestRune(v) { + lowercaseLettersCount++ + continue + } + } + + if lowercaseLettersCount == 0 { + return fmt.Errorf("password must contain at least 1 lowercase letter") + } + if uppercaseLettersCount == 0 { + return fmt.Errorf("password must contain at least 1 uppercase letter") + } + if specialCharsCount == 0 { + return fmt.Errorf("password must contain at least 1 special character") + } + return nil } diff --git a/src/core/utils/random.go b/src/core/utils/random.go deleted file mode 100644 index 1d2b70b..0000000 --- a/src/core/utils/random.go +++ /dev/null @@ -1,69 +0,0 @@ -package utils - -import ( - "math/rand" - "strings" - "time" -) - -type Charset int - -const ( - CharsetAll Charset = iota - CharsetLettersLower - CharsetLettersUpper - CharsetLetters - CharsetNumeric -) - -type charsetBlock struct { - Offset int - Size int -} - -func NewRand() *RandomUtil { - charsetLettersLower := charsetBlock{ - Offset: 0x41, - Size: 26, - } - - charsetLettersUpper := charsetBlock{ - Offset: 0x61, - Size: 26, - } - - charsetNumeric := charsetBlock{ - Offset: 0x30, - Size: 10, - } - - return &RandomUtil{ - charsets: map[Charset][]charsetBlock{ - CharsetNumeric: {charsetNumeric}, - CharsetLettersLower: {charsetLettersLower}, - CharsetLettersUpper: {charsetLettersUpper}, - CharsetLetters: {charsetLettersLower, charsetLettersUpper}, - CharsetAll: {charsetLettersLower, charsetLettersUpper, charsetNumeric}, - }, - } -} - -type RandomUtil struct { - charsets map[Charset][]charsetBlock -} - -func (r *RandomUtil) RandomID(outputLenght int, charset Charset) string { - src := rand.NewSource(time.Now().UnixMicro()) - randGen := rand.New(src) - - charsetBlocks := r.charsets[charset] - - builder := strings.Builder{} - for i := 0; i < outputLenght; i++ { - charsetBlock := charsetBlocks[randGen.Int()%len(charsetBlocks)] - - byte := charsetBlock.Offset + (randGen.Int() % charsetBlock.Size) - builder.WriteRune(rune(byte)) - } - return builder.String() -}