Skip to content

Commit

Permalink
fix: ensure GeneratePassword returns password with mixed cases (#8904)
Browse files Browse the repository at this point in the history
(cherry picked from commit 972c0d8)
  • Loading branch information
yipeng1030 committed Feb 10, 2025
1 parent 2adb863 commit 6ea123b
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,14 @@ func (t *clusterShardingAccountTransformer) buildPassword(transCtx *clusterTrans

func (t *clusterShardingAccountTransformer) generatePassword(account appsv1.SystemAccount) []byte {
config := account.PasswordGenerationPolicy
passwd, _ := common.GeneratePassword((int)(config.Length), (int)(config.NumDigits), (int)(config.NumSymbols), false, config.Seed)
passwd, _ := common.GeneratePassword((int)(config.Length), (int)(config.NumDigits), (int)(config.NumSymbols), config.Seed)
switch config.LetterCase {
case appsv1.UpperCases:
passwd = strings.ToUpper(passwd)
case appsv1.LowerCases:
passwd = strings.ToLower(passwd)
case appsv1.MixedCases:
passwd, _ = common.EnsureMixedCase(passwd, config.Seed)
}
return []byte(passwd)
}
Expand Down
4 changes: 3 additions & 1 deletion controllers/apps/component/transformer_component_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,14 @@ func (t *componentAccountTransformer) buildPassword(ctx *componentTransformConte

func (t *componentAccountTransformer) generatePassword(account synthesizedSystemAccount) []byte {
config := account.PasswordGenerationPolicy
passwd, _ := common.GeneratePassword((int)(config.Length), (int)(config.NumDigits), (int)(config.NumSymbols), false, config.Seed)
passwd, _ := common.GeneratePassword((int)(config.Length), (int)(config.NumDigits), (int)(config.NumSymbols), config.Seed)
switch config.LetterCase {
case appsv1.UpperCases:
passwd = strings.ToUpper(passwd)
case appsv1.LowerCases:
passwd = strings.ToLower(passwd)
case appsv1.MixedCases:
passwd, _ = common.EnsureMixedCase(passwd, config.Seed)
}
return []byte(passwd)
}
Expand Down
73 changes: 59 additions & 14 deletions pkg/common/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"encoding/binary"
mathrand "math/rand"
"time"
"unicode"

"github.com/sethvargo/go-password/password"
)
Expand All @@ -45,19 +46,11 @@ func (r *PasswordReader) Seed(seed int64) {
r.rand.Seed(seed)
}

// GeneratePassword generates a password with the given requirements and seed.
func GeneratePassword(length, numDigits, numSymbols int, noUpper bool, seed string) (string, error) {
var rand *mathrand.Rand
if len(seed) == 0 {
rand = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
} else {
h := sha256.New()
_, err := h.Write([]byte(seed))
if err != nil {
return "", err
}
uSeed := binary.BigEndian.Uint64(h.Sum(nil))
rand = mathrand.New(mathrand.NewSource(int64(uSeed)))
// GeneratePassword generates a password with the given requirements and seed in lowercase.
func GeneratePassword(length, numDigits, numSymbols int, seed string) (string, error) {
rand, err := newRngFromSeed(seed)
if err != nil {
return "", err
}
passwordReader := &PasswordReader{rand: rand}
gen, err := password.NewGenerator(&password.GeneratorInput{
Expand All @@ -70,5 +63,57 @@ func GeneratePassword(length, numDigits, numSymbols int, noUpper bool, seed stri
if err != nil {
return "", err
}
return gen.Generate(length, numDigits, numSymbols, noUpper, true)
return gen.Generate(length, numDigits, numSymbols, true, true)
}

// EnsureMixedCase randomizes the letter casing in the given string, ensuring
// that the result contains at least one uppercase and one lowercase letter
func EnsureMixedCase(in, seed string) (string, error) {
runes := []rune(in)
letterIndices := make([]int, 0, len(runes))
for i, r := range runes {
if unicode.IsLetter(r) {
letterIndices = append(letterIndices, i)
}
}
L := len(letterIndices)

if L < 2 {
return in, nil
}

// Get a random number x in [1, 2^L - 2], whose binary list will be used to determine the letter casing.
// avoid the all-0 and all-1 patterns, which cause all-lowercase and all-uppercase password.
rng, err := newRngFromSeed(seed)
if err != nil {
return in, err
}
x := uint64(rng.Int63n(int64((1<<L)-2))) + 1

for i := 0; i < L; i++ {
bit := (x >> i) & 1
idx := letterIndices[i]
if bit == 0 {
runes[idx] = unicode.ToLower(runes[idx])
} else {
runes[idx] = unicode.ToUpper(runes[idx])
}
}
return string(runes), nil
}

// newRngFromSeed initializes a *mathrand.Rand from the given seed. If seed is empty,
// it uses the current time, making the output non-deterministic.
func newRngFromSeed(seed string) (*mathrand.Rand, error) {
if seed == "" {
return mathrand.New(mathrand.NewSource(time.Now().UnixNano())), nil
}
// Convert seed string to a 64-bit integer via SHA-256
h := sha256.New()
if _, err := h.Write([]byte(seed)); err != nil {
return nil, err
}
sum := h.Sum(nil)
uSeed := binary.BigEndian.Uint64(sum)
return mathrand.New(mathrand.NewSource(int64(uSeed))), nil
}
79 changes: 75 additions & 4 deletions pkg/common/password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func testGeneratorGeneratePasswordWithSeed(t *testing.T) {
resultSeedFirstTime := ""
resultSeedEachTime := ""
for i := 0; i < N; i++ {
res, err := GeneratePassword(10, 5, 0, false, seed)
res, err := GeneratePassword(10, 5, 0, seed)
if err != nil {
t.Error(err)
}
Expand All @@ -52,11 +52,11 @@ func testGeneratorGeneratePassword(t *testing.T) {
t.Run("exceeds_length", func(t *testing.T) {
t.Parallel()

if _, err := GeneratePassword(0, 1, 0, false, ""); err != password.ErrExceedsTotalLength {
if _, err := GeneratePassword(0, 1, 0, ""); err != password.ErrExceedsTotalLength {
t.Errorf("expected %q to be %q", err, password.ErrExceedsTotalLength)
}

if _, err := GeneratePassword(0, 0, 1, false, ""); err != password.ErrExceedsTotalLength {
if _, err := GeneratePassword(0, 0, 1, ""); err != password.ErrExceedsTotalLength {
t.Errorf("expected %q to be %q", err, password.ErrExceedsTotalLength)
}
})
Expand All @@ -67,7 +67,7 @@ func testGeneratorGeneratePassword(t *testing.T) {
resultSeedEachTime := ""
hasDiffPassword := false
for i := 0; i < N; i++ {
res, err := GeneratePassword(i%len(password.LowerLetters), 0, 0, true, "")
res, err := GeneratePassword(i%(len(password.LowerLetters)+len(password.UpperLetters)), 0, 0, "")
if err != nil {
t.Error(err)
}
Expand All @@ -93,3 +93,74 @@ func TestGeneratorGeneratePassword(t *testing.T) {
func TestGeneratorGeneratePasswordWithSeed(t *testing.T) {
testGeneratorGeneratePasswordWithSeed(t)
}

// containsUppercase checks if s has at least one uppercase letter (A-Z).
func containsUppercase(s string) bool {
for _, r := range s {
if r >= 'A' && r <= 'Z' {
return true
}
}
return false
}

// containsLowercase checks if s has at least one lowercase letter (a-z).
func containsLowercase(s string) bool {
for _, r := range s {
if r >= 'a' && r <= 'z' {
return true
}
}
return false
}

// TestGeneratorEnsureMixedCase verifies two requirements:
// 1) When noUpper = false, the generated password contains uppercase and lowercase letters.
// 2) Passwords generated with the same seed are identical.
func TestGeneratorEnsureMixedCase(t *testing.T) {
t.Run("should_contain_mixed_case_when_noUpper_false", func(t *testing.T) {
length := 12
numDigits := 3
numSymbols := 2
seed := ""

// Generate multiple passwords and check they have both upper and lower letters.
for i := 0; i < 100; i++ {
pwd, err := GeneratePassword(length, numDigits, numSymbols, seed)
if err != nil {
t.Fatalf("unexpected error generating password: %v", err)
}
pwd, err = EnsureMixedCase(pwd, seed)
if err != nil {
t.Fatalf("unexpected error Ensuring mixed-case password: %v", err)
}
if !containsUppercase(pwd) || !containsLowercase(pwd) {
t.Errorf("password %q does not contain both uppercase and lowercase letters", pwd)
}
}
})

t.Run("should_produce_same_result_with_same_seed", func(t *testing.T) {
length := 10
numDigits := 2
numSymbols := 1
seed := "fixed-seed-123"

var firstPwd string
for i := 0; i < 50; i++ {
pwd, err := GeneratePassword(length, numDigits, numSymbols, seed)
if err != nil {
t.Fatalf("unexpected error generating password with seed: %v", err)
}
pwd, err = EnsureMixedCase(pwd, seed)
if err != nil {
t.Fatalf("unexpected error Ensuring mixed-case password: %v", err)
}
if i == 0 {
firstPwd = pwd
} else if pwd != firstPwd {
t.Errorf("expected the same password for the same seed, but got %q vs %q", firstPwd, pwd)
}
}
})
}

0 comments on commit 6ea123b

Please sign in to comment.