diff options
Diffstat (limited to 'modules/password')
-rw-r--r-- | modules/password/password.go | 126 | ||||
-rw-r--r-- | modules/password/password_test.go | 76 | ||||
-rw-r--r-- | modules/password/pwn.go | 28 | ||||
-rw-r--r-- | modules/password/pwn/pwn.go | 118 | ||||
-rw-r--r-- | modules/password/pwn/pwn_test.go | 142 |
5 files changed, 0 insertions, 490 deletions
diff --git a/modules/password/password.go b/modules/password/password.go deleted file mode 100644 index fe2a2a7bd5..0000000000 --- a/modules/password/password.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package password - -import ( - "bytes" - goContext "context" - "crypto/rand" - "math/big" - "strings" - "sync" - - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/setting" -) - -// complexity contains information about a particular kind of password complexity -type complexity struct { - ValidChars string - TrNameOne string -} - -var ( - matchComplexityOnce sync.Once - validChars string - requiredList []complexity - - charComplexities = map[string]complexity{ - "lower": { - `abcdefghijklmnopqrstuvwxyz`, - "form.password_lowercase_one", - }, - "upper": { - `ABCDEFGHIJKLMNOPQRSTUVWXYZ`, - "form.password_uppercase_one", - }, - "digit": { - `0123456789`, - "form.password_digit_one", - }, - "spec": { - ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`", - "form.password_special_one", - }, - } -) - -// NewComplexity for preparation -func NewComplexity() { - matchComplexityOnce.Do(func() { - setupComplexity(setting.PasswordComplexity) - }) -} - -func setupComplexity(values []string) { - if len(values) != 1 || values[0] != "off" { - for _, val := range values { - if complex, ok := charComplexities[val]; ok { - validChars += complex.ValidChars - requiredList = append(requiredList, complex) - } - } - if len(requiredList) == 0 { - // No valid character classes found; use all classes as default - for _, complex := range charComplexities { - validChars += complex.ValidChars - requiredList = append(requiredList, complex) - } - } - } - if validChars == "" { - // No complexities to check; provide a sensible default for password generation - validChars = charComplexities["lower"].ValidChars + charComplexities["upper"].ValidChars + charComplexities["digit"].ValidChars - } -} - -// IsComplexEnough return True if password meets complexity settings -func IsComplexEnough(pwd string) bool { - NewComplexity() - if len(validChars) > 0 { - for _, req := range requiredList { - if !strings.ContainsAny(req.ValidChars, pwd) { - return false - } - } - } - return true -} - -// Generate a random password -func Generate(n int) (string, error) { - NewComplexity() - buffer := make([]byte, n) - max := big.NewInt(int64(len(validChars))) - for { - for j := 0; j < n; j++ { - rnd, err := rand.Int(rand.Reader, max) - if err != nil { - return "", err - } - buffer[j] = validChars[rnd.Int64()] - } - pwned, err := IsPwned(goContext.Background(), string(buffer)) - if err != nil { - return "", err - } - if IsComplexEnough(string(buffer)) && !pwned && string(buffer[0]) != " " && string(buffer[n-1]) != " " { - return string(buffer), nil - } - } -} - -// BuildComplexityError builds the error message when password complexity checks fail -func BuildComplexityError(ctx *context.Context) string { - var buffer bytes.Buffer - buffer.WriteString(ctx.Tr("form.password_complexity")) - buffer.WriteString("<ul>") - for _, c := range requiredList { - buffer.WriteString("<li>") - buffer.WriteString(ctx.Tr(c.TrNameOne)) - buffer.WriteString("</li>") - } - buffer.WriteString("</ul>") - return buffer.String() -} diff --git a/modules/password/password_test.go b/modules/password/password_test.go deleted file mode 100644 index 6c35dc86bd..0000000000 --- a/modules/password/password_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package password - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestComplexity_IsComplexEnough(t *testing.T) { - matchComplexityOnce.Do(func() {}) - - testlist := []struct { - complexity []string - truevalues []string - falsevalues []string - }{ - {[]string{"off"}, []string{"1", "-", "a", "A", "ñ", "日本語"}, []string{}}, - {[]string{"lower"}, []string{"abc", "abc!"}, []string{"ABC", "123", "=!$", ""}}, - {[]string{"upper"}, []string{"ABC"}, []string{"abc", "123", "=!$", "abc!", ""}}, - {[]string{"digit"}, []string{"123"}, []string{"abc", "ABC", "=!$", "abc!", ""}}, - {[]string{"spec"}, []string{"=!$", "abc!"}, []string{"abc", "ABC", "123", ""}}, - {[]string{"off"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}, nil}, - {[]string{"lower", "spec"}, []string{"abc!"}, []string{"abc", "ABC", "123", "=!$", "abcABC123", ""}}, - {[]string{"lower", "upper", "digit"}, []string{"abcABC123"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}}, - {[]string{""}, []string{"abC=1", "abc!9D"}, []string{"ABC", "123", "=!$", ""}}, - } - - for _, test := range testlist { - testComplextity(test.complexity) - for _, val := range test.truevalues { - assert.True(t, IsComplexEnough(val)) - } - for _, val := range test.falsevalues { - assert.False(t, IsComplexEnough(val)) - } - } - - // Remove settings for other tests - testComplextity([]string{"off"}) -} - -func TestComplexity_Generate(t *testing.T) { - matchComplexityOnce.Do(func() {}) - - const maxCount = 50 - const pwdLen = 50 - - test := func(t *testing.T, modes []string) { - testComplextity(modes) - for i := 0; i < maxCount; i++ { - pwd, err := Generate(pwdLen) - assert.NoError(t, err) - assert.Len(t, pwd, pwdLen) - assert.True(t, IsComplexEnough(pwd), "Failed complexities with modes %+v for generated: %s", modes, pwd) - } - } - - test(t, []string{"lower"}) - test(t, []string{"upper"}) - test(t, []string{"lower", "upper", "spec"}) - test(t, []string{"off"}) - test(t, []string{""}) - - // Remove settings for other tests - testComplextity([]string{"off"}) -} - -func testComplextity(values []string) { - // Cleanup previous values - validChars = "" - requiredList = make([]complexity, 0, len(values)) - setupComplexity(values) -} diff --git a/modules/password/pwn.go b/modules/password/pwn.go deleted file mode 100644 index 91bad0d25b..0000000000 --- a/modules/password/pwn.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package password - -import ( - "context" - - "code.gitea.io/gitea/modules/password/pwn" - "code.gitea.io/gitea/modules/setting" -) - -// IsPwned checks whether a password has been pwned -// NOTE: This func returns true if it encounters an error under the assumption that you ALWAYS want to check against -// HIBP, so not getting a response should block a password until it can be verified. -func IsPwned(ctx context.Context, password string) (bool, error) { - if !setting.PasswordCheckPwn { - return false, nil - } - - client := pwn.New(pwn.WithContext(ctx)) - count, err := client.CheckPassword(password, true) - if err != nil { - return true, err - } - - return count > 0, nil -} diff --git a/modules/password/pwn/pwn.go b/modules/password/pwn/pwn.go deleted file mode 100644 index b5a015fb9c..0000000000 --- a/modules/password/pwn/pwn.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package pwn - -import ( - "context" - "crypto/sha1" - "encoding/hex" - "errors" - "fmt" - "io" - "net/http" - "strconv" - "strings" - - "code.gitea.io/gitea/modules/setting" -) - -const passwordURL = "https://api.pwnedpasswords.com/range/" - -// ErrEmptyPassword is an empty password error -var ErrEmptyPassword = errors.New("password cannot be empty") - -// Client is a HaveIBeenPwned client -type Client struct { - ctx context.Context - http *http.Client -} - -// New returns a new HaveIBeenPwned Client -func New(options ...ClientOption) *Client { - client := &Client{ - ctx: context.Background(), - http: http.DefaultClient, - } - - for _, opt := range options { - opt(client) - } - - return client -} - -// ClientOption is a way to modify a new Client -type ClientOption func(*Client) - -// WithHTTP will set the http.Client of a Client -func WithHTTP(httpClient *http.Client) func(pwnClient *Client) { - return func(pwnClient *Client) { - pwnClient.http = httpClient - } -} - -// WithContext will set the context.Context of a Client -func WithContext(ctx context.Context) func(pwnClient *Client) { - return func(pwnClient *Client) { - pwnClient.ctx = ctx - } -} - -func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*http.Request, error) { - req, err := http.NewRequestWithContext(ctx, method, url, body) - if err != nil { - return nil, err - } - req.Header.Add("User-Agent", "Gitea "+setting.AppVer) - return req, nil -} - -// CheckPassword returns the number of times a password has been compromised -// Adding padding will make requests more secure, however is also slower -// because artificial responses will be added to the response -// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ -func (c *Client) CheckPassword(pw string, padding bool) (int, error) { - if strings.TrimSpace(pw) == "" { - return -1, ErrEmptyPassword - } - - sha := sha1.New() - sha.Write([]byte(pw)) - enc := hex.EncodeToString(sha.Sum(nil)) - prefix, suffix := enc[:5], enc[5:] - - req, err := newRequest(c.ctx, http.MethodGet, fmt.Sprintf("%s%s", passwordURL, prefix), nil) - if err != nil { - return -1, nil - } - if padding { - req.Header.Add("Add-Padding", "true") - } - - resp, err := c.http.Do(req) - if err != nil { - return -1, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return -1, err - } - defer resp.Body.Close() - - for _, pair := range strings.Split(string(body), "\n") { - parts := strings.Split(pair, ":") - if len(parts) != 2 { - continue - } - if strings.EqualFold(suffix, parts[0]) { - count, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64) - if err != nil { - return -1, err - } - return int(count), nil - } - } - return 0, nil -} diff --git a/modules/password/pwn/pwn_test.go b/modules/password/pwn/pwn_test.go deleted file mode 100644 index 148208b964..0000000000 --- a/modules/password/pwn/pwn_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package pwn - -import ( - "errors" - "math/rand" - "net/http" - "os" - "strings" - "testing" - "time" -) - -var client = New(WithHTTP(&http.Client{ - Timeout: time.Second * 2, -})) - -func TestMain(m *testing.M) { - rand.Seed(time.Now().Unix()) - os.Exit(m.Run()) -} - -func TestPassword(t *testing.T) { - // Check input error - _, err := client.CheckPassword("", false) - if err == nil { - t.Log("blank input should return an error") - t.Fail() - } - if !errors.Is(err, ErrEmptyPassword) { - t.Log("blank input should return ErrEmptyPassword") - t.Fail() - } - - // Should fail - fail := "password1234" - count, err := client.CheckPassword(fail, false) - if err != nil { - t.Log(err) - t.Fail() - } - if count == 0 { - t.Logf("%s should fail as a password\n", fail) - t.Fail() - } - - // Should fail (with padding) - failPad := "administrator" - count, err = client.CheckPassword(failPad, true) - if err != nil { - t.Log(err) - t.Fail() - } - if count == 0 { - t.Logf("%s should fail as a password\n", failPad) - t.Fail() - } - - // Checking for a "good" password isn't going to be perfect, but we can give it a good try - // with hopefully minimal error. Try five times? - var good bool - var pw string - for idx := 0; idx <= 5; idx++ { - pw = testPassword() - count, err = client.CheckPassword(pw, false) - if err != nil { - t.Log(err) - t.Fail() - } - if count == 0 { - good = true - break - } - } - if !good { - t.Log("no generated passwords passed. there is a chance this is a fluke") - t.Fail() - } - - // Again, but with padded responses - good = false - for idx := 0; idx <= 5; idx++ { - pw = testPassword() - count, err = client.CheckPassword(pw, true) - if err != nil { - t.Log(err) - t.Fail() - } - if count == 0 { - good = true - break - } - } - if !good { - t.Log("no generated passwords passed. there is a chance this is a fluke") - t.Fail() - } -} - -// Credit to https://golangbyexample.com/generate-random-password-golang/ -// DO NOT USE THIS FOR AN ACTUAL PASSWORD GENERATOR -var ( - lowerCharSet = "abcdedfghijklmnopqrst" - upperCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - specialCharSet = "!@#$%&*" - numberSet = "0123456789" - allCharSet = lowerCharSet + upperCharSet + specialCharSet + numberSet -) - -func testPassword() string { - var password strings.Builder - - // Set special character - for i := 0; i < 5; i++ { - random := rand.Intn(len(specialCharSet)) - password.WriteString(string(specialCharSet[random])) - } - - // Set numeric - for i := 0; i < 5; i++ { - random := rand.Intn(len(numberSet)) - password.WriteString(string(numberSet[random])) - } - - // Set uppercase - for i := 0; i < 5; i++ { - random := rand.Intn(len(upperCharSet)) - password.WriteString(string(upperCharSet[random])) - } - - for i := 0; i < 5; i++ { - random := rand.Intn(len(allCharSet)) - password.WriteString(string(allCharSet[random])) - } - inRune := []rune(password.String()) - rand.Shuffle(len(inRune), func(i, j int) { - inRune[i], inRune[j] = inRune[j], inRune[i] - }) - return string(inRune) -} |