// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package random import ( "context" "crypto/rand" "encoding/json" "fmt" "io" "math" MRAND "math/rand" "reflect" "sort" "testing" "time" ) func TestStringGenerator_Generate_successful(t *testing.T) { type testCase struct { timeout time.Duration generator *StringGenerator } tests := map[string]testCase{ "common rules": { timeout: 1 * time.Second, generator: &StringGenerator{ Length: 20, Rules: []Rule{ CharsetRule{ Charset: LowercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: UppercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: NumericRuneset, MinChars: 1, }, CharsetRule{ Charset: ShortSymbolRuneset, MinChars: 1, }, }, charset: AlphaNumericShortSymbolRuneset, }, }, "charset not explicitly specified": { timeout: 1 * time.Second, generator: &StringGenerator{ Length: 20, Rules: []Rule{ CharsetRule{ Charset: LowercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: UppercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: NumericRuneset, MinChars: 1, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { // One context to rule them all, one context to find them, one context to bring them all and in the darkness bind them. ctx, cancel := context.WithTimeout(context.Background(), test.timeout) defer cancel() runeset := map[rune]bool{} runesFound := []rune{} for i := 0; i < 100; i++ { actual, err := test.generator.Generate(ctx, nil) if err != nil { t.Fatalf("no error expected, but got: %s", err) } for _, r := range actual { if runeset[r] { continue } runeset[r] = true runesFound = append(runesFound, r) } } sort.Sort(runes(runesFound)) expectedCharset := getChars(test.generator.Rules) if !reflect.DeepEqual(runesFound, expectedCharset) { t.Fatalf("Didn't find all characters from the charset\nActual : [%s]\nExpected: [%s]", string(runesFound), string(expectedCharset)) } }) } } func TestStringGenerator_Generate_errors(t *testing.T) { type testCase struct { timeout time.Duration generator *StringGenerator rng io.Reader } tests := map[string]testCase{ "already timed out": { timeout: 0, generator: &StringGenerator{ Length: 20, Rules: []Rule{ testCharsetRule{ fail: false, }, }, charset: AlphaNumericShortSymbolRuneset, }, rng: rand.Reader, }, "impossible rules": { timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long generator: &StringGenerator{ Length: 20, Rules: []Rule{ testCharsetRule{ fail: true, }, }, charset: AlphaNumericShortSymbolRuneset, }, rng: rand.Reader, }, "bad RNG reader": { timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long generator: &StringGenerator{ Length: 20, Rules: []Rule{}, charset: AlphaNumericShortSymbolRuneset, }, rng: badReader{}, }, "0 length": { timeout: 10 * time.Millisecond, generator: &StringGenerator{ Length: 0, Rules: []Rule{ CharsetRule{ Charset: []rune("abcde"), MinChars: 0, }, }, charset: []rune("abcde"), }, rng: rand.Reader, }, "-1 length": { timeout: 10 * time.Millisecond, generator: &StringGenerator{ Length: -1, Rules: []Rule{ CharsetRule{ Charset: []rune("abcde"), MinChars: 0, }, }, charset: []rune("abcde"), }, rng: rand.Reader, }, "no charset": { timeout: 10 * time.Millisecond, generator: &StringGenerator{ Length: 20, Rules: []Rule{}, }, rng: rand.Reader, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { // One context to rule them all, one context to find them, one context to bring them all and in the darkness bind them. ctx, cancel := context.WithTimeout(context.Background(), test.timeout) defer cancel() actual, err := test.generator.Generate(ctx, test.rng) if err == nil { t.Fatalf("Expected error but none found") } if actual != "" { t.Fatalf("Random string returned: %s", actual) } }) } } func TestRandomRunes_deterministic(t *testing.T) { // These tests are to ensure that the charset selection doesn't do anything weird like selecting the same character // over and over again. The number of test cases here should be kept to a minimum since they are sensitive to changes type testCase struct { rngSeed int64 charset string length int expected string } tests := map[string]testCase{ "small charset": { rngSeed: 1585593298447807000, charset: "abcde", length: 20, expected: "ddddddcdebbeebdbdbcd", }, "common charset": { rngSeed: 1585593298447807001, charset: AlphaNumericShortSymbolCharset, length: 20, expected: "ON6lVjnBs84zJbUBVEzb", }, "max size charset": { rngSeed: 1585593298447807002, charset: " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠", length: 20, expected: "tųŎ℄ņ℃Œ.@řHš-ℍ}ħGIJLℏ", }, } for name, test := range tests { t.Run(name, func(t *testing.T) { rng := MRAND.New(MRAND.NewSource(test.rngSeed)) runes, err := randomRunes(rng, []rune(test.charset), test.length) if err != nil { t.Fatalf("Expected no error, but found: %s", err) } str := string(runes) if str != test.expected { t.Fatalf("Actual: %s Expected: %s", str, test.expected) } }) } } func TestRandomRunes_successful(t *testing.T) { type testCase struct { charset []rune // Assumes no duplicate runes length int } tests := map[string]testCase{ "small charset": { charset: []rune("abcde"), length: 20, }, "common charset": { charset: AlphaNumericShortSymbolRuneset, length: 20, }, "max size charset": { charset: []rune( " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠", ), length: 20, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { runeset := map[rune]bool{} runesFound := []rune{} for i := 0; i < 10000; i++ { actual, err := randomRunes(rand.Reader, test.charset, test.length) if err != nil { t.Fatalf("no error expected, but got: %s", err) } for _, r := range actual { if runeset[r] { continue } runeset[r] = true runesFound = append(runesFound, r) } } sort.Sort(runes(runesFound)) // Sort the input too just to ensure that they can be compared sort.Sort(runes(test.charset)) if !reflect.DeepEqual(runesFound, test.charset) { t.Fatalf("Didn't find all characters from the charset\nActual : [%s]\nExpected: [%s]", string(runesFound), string(test.charset)) } }) } } func TestRandomRunes_errors(t *testing.T) { type testCase struct { charset []rune length int rng io.Reader } tests := map[string]testCase{ "nil charset": { charset: nil, length: 20, rng: rand.Reader, }, "empty charset": { charset: []rune{}, length: 20, rng: rand.Reader, }, "charset is too long": { charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠" + "Σ", ), length: 20, rng: rand.Reader, }, "length is zero": { charset: []rune("abcde"), length: 0, rng: rand.Reader, }, "length is negative": { charset: []rune("abcde"), length: -3, rng: rand.Reader, }, "reader failed": { charset: []rune("abcde"), length: 20, rng: badReader{}, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { actual, err := randomRunes(test.rng, test.charset, test.length) if err == nil { t.Fatalf("Expected error but none found") } if actual != nil { t.Fatalf("Expected no value, but found [%s]", string(actual)) } }) } } func BenchmarkStringGenerator_Generate(b *testing.B) { lengths := []int{ 8, 12, 16, 20, 24, 28, } type testCase struct { generator *StringGenerator } benches := map[string]testCase{ "no restrictions": { generator: &StringGenerator{ Rules: []Rule{ CharsetRule{ Charset: AlphaNumericFullSymbolRuneset, }, }, }, }, "default generator": { generator: DefaultStringGenerator, }, "large symbol set": { generator: &StringGenerator{ Rules: []Rule{ CharsetRule{ Charset: LowercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: UppercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: NumericRuneset, MinChars: 1, }, CharsetRule{ Charset: FullSymbolRuneset, MinChars: 1, }, }, }, }, "max symbol set": { generator: &StringGenerator{ Rules: []Rule{ CharsetRule{ Charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" + "ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" + "šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠"), }, CharsetRule{ Charset: LowercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: UppercaseRuneset, MinChars: 1, }, CharsetRule{ Charset: []rune("ĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒ"), MinChars: 1, }, }, }, }, "restrictive charset rules": { generator: &StringGenerator{ Rules: []Rule{ CharsetRule{ Charset: AlphaNumericShortSymbolRuneset, }, CharsetRule{ Charset: []rune("A"), MinChars: 1, }, CharsetRule{ Charset: []rune("1"), MinChars: 1, }, CharsetRule{ Charset: []rune("a"), MinChars: 1, }, CharsetRule{ Charset: []rune("-"), MinChars: 1, }, }, }, }, } for name, bench := range benches { b.Run(name, func(b *testing.B) { for _, length := range lengths { bench.generator.Length = length b.Run(fmt.Sprintf("length=%d", length), func(b *testing.B) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() b.ResetTimer() for i := 0; i < b.N; i++ { str, err := bench.generator.Generate(ctx, nil) if err != nil { b.Fatalf("Failed to generate string: %s", err) } if str == "" { b.Fatalf("Didn't error but didn't generate a string") } } }) } }) } // Mimic what the SQLCredentialsProducer is doing b.Run("SQLCredentialsProducer", func(b *testing.B) { sg := StringGenerator{ Length: 16, // 16 because the SQLCredentialsProducer prepends 4 characters to a 20 character password charset: AlphaNumericRuneset, Rules: nil, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() b.ResetTimer() for i := 0; i < b.N; i++ { str, err := sg.Generate(ctx, nil) if err != nil { b.Fatalf("Failed to generate string: %s", err) } if str == "" { b.Fatalf("Didn't error but didn't generate a string") } } }) } // Ensure the StringGenerator can be properly JSON-ified func TestStringGenerator_JSON(t *testing.T) { expected := StringGenerator{ Length: 20, charset: deduplicateRunes([]rune("teststring" + ShortSymbolCharset)), Rules: []Rule{ testCharsetRule{ String: "teststring", Integer: 123, }, CharsetRule{ Charset: ShortSymbolRuneset, MinChars: 1, }, }, } b, err := json.Marshal(expected) if err != nil { t.Fatalf("Failed to marshal to JSON: %s", err) } parser := PolicyParser{ RuleRegistry: Registry{ Rules: map[string]ruleConstructor{ "testrule": newTestRule, "charset": ParseCharset, }, }, } actual, err := parser.ParsePolicy(string(b)) if err != nil { t.Fatalf("Failed to parse JSON: %s", err) } if !reflect.DeepEqual(actual, expected) { t.Fatalf("Actual: %#v\nExpected: %#v", actual, expected) } } type badReader struct{} func (badReader) Read([]byte) (int, error) { return 0, fmt.Errorf("test error") } func TestValidate(t *testing.T) { type testCase struct { generator *StringGenerator expectErr bool } tests := map[string]testCase{ "default generator": { generator: DefaultStringGenerator, expectErr: false, }, "length is 0": { generator: &StringGenerator{ Length: 0, }, expectErr: true, }, "length is negative": { generator: &StringGenerator{ Length: -2, }, expectErr: true, }, "nil charset, no rules": { generator: &StringGenerator{ Length: 5, charset: nil, }, expectErr: true, }, "zero length charset, no rules": { generator: &StringGenerator{ Length: 5, charset: []rune{}, }, expectErr: true, }, "rules require password longer than length": { generator: &StringGenerator{ Length: 5, charset: []rune("abcde"), Rules: []Rule{ CharsetRule{ Charset: []rune("abcde"), MinChars: 6, }, }, }, expectErr: true, }, "charset has non-printable characters": { generator: &StringGenerator{ Length: 0, charset: []rune{ 'a', 'b', 0, // Null character 'd', 'e', }, }, expectErr: true, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { err := test.generator.validateConfig() if test.expectErr && err == nil { t.Fatalf("err expected, got nil") } if !test.expectErr && err != nil { t.Fatalf("no error expected, got: %s", err) } }) } } type testNonCharsetRule struct { String string `mapstructure:"string" json:"string"` } func (tr testNonCharsetRule) Pass([]rune) bool { return true } func (tr testNonCharsetRule) Type() string { return "testNonCharsetRule" } func TestGetChars(t *testing.T) { type testCase struct { rules []Rule expected []rune } tests := map[string]testCase{ "nil rules": { rules: nil, expected: []rune(nil), }, "empty rules": { rules: []Rule{}, expected: []rune(nil), }, "rule without chars": { rules: []Rule{ testNonCharsetRule{ String: "teststring", }, }, expected: []rune(nil), }, "rule with chars": { rules: []Rule{ CharsetRule{ Charset: []rune("abcdefghij"), MinChars: 1, }, }, expected: []rune("abcdefghij"), }, } for name, test := range tests { t.Run(name, func(t *testing.T) { actual := getChars(test.rules) if !reflect.DeepEqual(actual, test.expected) { t.Fatalf("Actual: %v\nExpected: %v", actual, test.expected) } }) } } func TestDeduplicateRunes(t *testing.T) { type testCase struct { input []rune expected []rune } tests := map[string]testCase{ "empty string": { input: []rune(""), expected: []rune(nil), }, "no duplicates": { input: []rune("abcde"), expected: []rune("abcde"), }, "in order duplicates": { input: []rune("aaaabbbbcccccccddddeeeee"), expected: []rune("abcde"), }, "out of order duplicates": { input: []rune("abcdeabcdeabcdeabcde"), expected: []rune("abcde"), }, "unicode no duplicates": { input: []rune("日本語"), expected: []rune("日本語"), }, "unicode in order duplicates": { input: []rune("日日日日本本本語語語語語"), expected: []rune("日本語"), }, "unicode out of order duplicates": { input: []rune("日本語日本語日本語日本語"), expected: []rune("日本語"), }, } for name, test := range tests { t.Run(name, func(t *testing.T) { actual := deduplicateRunes(test.input) if !reflect.DeepEqual(actual, test.expected) { t.Fatalf("Actual: %#v\nExpected:%#v", actual, test.expected) } }) } } func TestRandomRunes_Bias(t *testing.T) { type testCase struct { charset []rune maxStdDev float64 } tests := map[string]testCase{ "small charset": { charset: []rune("abcde"), maxStdDev: 2700, }, "lowercase characters": { charset: LowercaseRuneset, maxStdDev: 1000, }, "alphabetical characters": { charset: AlphabeticRuneset, maxStdDev: 800, }, "alphanumeric": { charset: AlphaNumericRuneset, maxStdDev: 800, }, "alphanumeric with symbol": { charset: AlphaNumericShortSymbolRuneset, maxStdDev: 800, }, "charset evenly divisible into 256": { charset: append(AlphaNumericRuneset, '!', '@'), maxStdDev: 800, }, "large charset": { charset: FullSymbolRuneset, maxStdDev: 800, }, "just under half size charset": { charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğ"), maxStdDev: 800, }, "half size charset": { charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ"), maxStdDev: 800, }, } for name, test := range tests { t.Run(fmt.Sprintf("%s (%d chars)", name, len(test.charset)), func(t *testing.T) { runeCounts := map[rune]int{} generations := 50000 length := 100 for i := 0; i < generations; i++ { str, err := randomRunes(nil, test.charset, length) if err != nil { t.Fatal(err) } for _, r := range str { runeCounts[r]++ } } chars := charCounts{} var sum float64 for r, count := range runeCounts { chars = append(chars, charCount{r, count}) sum += float64(count) } mean := sum / float64(len(runeCounts)) var stdDev float64 for _, count := range runeCounts { stdDev += math.Pow(float64(count)-mean, 2) } stdDev = math.Sqrt(stdDev / float64(len(runeCounts))) t.Logf("Mean : %10.4f", mean) if stdDev > test.maxStdDev { t.Fatalf("Standard deviation is too large: %.2f > %.2f", stdDev, test.maxStdDev) } }) } } type charCount struct { r rune count int } type charCounts []charCount func (s charCounts) Len() int { return len(s) } func (s charCounts) Less(i, j int) bool { return s[i].r < s[j].r } func (s charCounts) Swap(i, j int) { s[i], s[j] = s[j], s[i] }