diff --git a/pkg/splunk/common/names.go b/pkg/splunk/common/names.go index 32e892b96..08fdf0536 100644 --- a/pkg/splunk/common/names.go +++ b/pkg/splunk/common/names.go @@ -32,6 +32,17 @@ const ( // SecretBytes used to generate Splunk secrets SecretBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + // SecretBytes with complexity used to generate Splunk secrets with complexity + SecretBytesLower = "abcdefghijklmnopqrstuvwxyz" + SecretBytesUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + SecretBytesDecimal = "0123456789" + // we dont use $ here to prevent secret starting with this as this is used by Splunk to identify obfuscated one + SecretBytesSpecial = "-*&%#@,.;:/?[]{}+=-_<>" + // version with all possible characters use to complete after we have match minimal complexity + SecretBytesComplete = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-*&%#@,.;:/?[]{}+=-_<>" + // version when complexity disabled + SecretBytesSimple = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + // HexBytes used to generate random hexadecimal strings (e.g. HEC tokens) HexBytes = "ABCDEF01234567890" diff --git a/pkg/splunk/common/util.go b/pkg/splunk/common/util.go index 42cf832d3..7e254765f 100644 --- a/pkg/splunk/common/util.go +++ b/pkg/splunk/common/util.go @@ -98,11 +98,110 @@ func GetServiceFQDN(namespace string, name string) string { ) } +// SecretBytes is the string with characters allowed to +// m is number of characters to generate +// b is they byte array to modify (which already exist) +func GenerateSecretPartWithComplexity(SecretBytes string, m int, b []byte) error { + j := 0 + k := 0 + length := len(b) + if m > 0 { + brokeEarly := false + for i := 1; i < m+1; i++ { + maxtry := 100 + for j = 1; j < maxtry; j++ { + // we try a random position from 0 to length-1 where length is secret size + // Use crypto/rand to get a secure random index + var indexByte [1]byte + _, err := rand.Read(indexByte[0:1]) // 0:1 turn array into slice to be used with Read and the function will put the random value in indexByte[0] + if err != nil { + // note : we may lack entropy and be running out of randomness + return err + } + // compute the random position number + k = int(indexByte[0]) % length + if b[k] == 0 { + _, err = rand.Read(indexByte[0:1]) // 0:1 turn array into slice to be used with Read and the function will put the random value in indexByte[0] + if err != nil { + // note : we may lack entropy and be running out of randomness + return err + } + // this was not yet assigned a value + b[k] = SecretBytes[int(indexByte[0])%len(SecretBytes)] + brokeEarly = true + break + } else { + //fmt.Printf("position k %d already used will try another position\n ", k) + } + } + } + if brokeEarly { + //fmt.Printf("generation ended succesfully \n") + } else { + return fmt.Errorf("generation was not completed after %d maxtry, something is wrong\n", m) + } + } else if m == 0 { + //fmt.Println("no complexity requirement for this type") + } else { + return fmt.Errorf("incorrect value for minimal complexity, ignoring") + } + return nil +} + +func GenerateSecretWithComplexity(n int, minlower int, minupper int, mindecimal int, minspecial int) ([]byte, error) { + b := make([]byte, n) + if n < minlower+minupper+mindecimal+minspecial { + fmt.Printf("password length and complexity requirements are incompatible length=%d, minlower=%d , minupper=%d, mindecimal=%d, minspecial=%d\n", n, minlower, minupper, mindecimal, minspecial) + // b is empty here , we return error and expect caller to check for it + return b, fmt.Errorf("password length and complexity requirements are incompatible length=%d, minlower=%d , minupper=%d, mindecimal=%d, minspecial=%d\n", n, minlower, minupper, mindecimal, minspecial) + } else if minlower+minupper+mindecimal+minspecial == 0 { + // disable complexity , we also use SecretBytesNormal instead of SecretBytesComplete + for i := range b { + // Use crypto/rand to get a secure random index + var indexByte [1]byte + _, err := rand.Read(indexByte[0:1]) // 0:1 turn array into slice to be used with Read and the function will put the random value in indexByte[0] + if err != nil { + return b, err + } + + b[i] = SecretBytesSimple[int(indexByte[0])%len(SecretBytesSimple)] + } + } else { + //fmt.Printf("password length and complexity requirements are OK length=%d, minlower=%d , minupper=%d, mindecimal=%d, minspecial=%d\n", n, minlower, minupper, mindecimal, minspecial) + + GenerateSecretPartWithComplexity(SecretBytesLower, minlower, b) + GenerateSecretPartWithComplexity(SecretBytesUpper, minupper, b) + GenerateSecretPartWithComplexity(SecretBytesDecimal, mindecimal, b) + GenerateSecretPartWithComplexity(SecretBytesSpecial, minspecial, b) + // complete gaps + for i := range b { + if b[i] == 0 { + // we try a random position from 0 to length-1 where length is secret size + // Use crypto/rand to get a secure random index + var indexByte [1]byte + _, err := rand.Read(indexByte[0:1]) // 0:1 turn array into slice to be used with Read and the function will put the random value in indexByte[0] + if err != nil { + return b, err + } + + b[i] = SecretBytesComplete[int(indexByte[0])%len(SecretBytesComplete)] + } + } + } + return b, nil +} + // GenerateSecret returns a randomly generated sequence of text that is n bytes in length. func GenerateSecret(SecretBytes string, n int) []byte { b := make([]byte, n) for i := range b { - b[i] = SecretBytes[rand.Int63()%int64(len(SecretBytes))] + // Use crypto/rand to get a secure random index + var indexByte [1]byte + _, err := rand.Read(indexByte[0:1]) // 0:1 turn array into slice to be used with Read and the function will put the random value in indexByte[0] + if err != nil { + return nil + } + b[i] = SecretBytes[int(indexByte[0])%len(SecretBytes)] } return b } diff --git a/pkg/splunk/common/util_test.go b/pkg/splunk/common/util_test.go index c92d00d86..bc55ff1ba 100644 --- a/pkg/splunk/common/util_test.go +++ b/pkg/splunk/common/util_test.go @@ -189,6 +189,67 @@ func TestGenerateSecret(t *testing.T) { test("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 10) } +func TestGenerateSecretWithComplexity(t *testing.T) { + test := func(n int, minlower int, minupper int, mindecimal int, minspecial int) { + var err error + results := [][]byte{} + b := make([]byte, n) + // get 100 results + for i := 0; i < 100; i++ { + b, err = GenerateSecretWithComplexity(n, minlower, minupper, mindecimal, minspecial) + if err != nil { + fmt.Printf("GenerateSecretWithComplexity(%d,%d,%d,%d,%d) returned error %s", n, minlower, minupper, mindecimal, minspecial, err) + break + } else { + + results = append(results, b) + + // ensure its length is correct + if len(results[i]) != n { + fmt.Printf("GenerateSecretWithComplexity(%d,%d,%d,%d,%d) len = %d; want %d", n, minlower, minupper, mindecimal, minspecial, len(results[i]), n) + } + + // ensure it only includes allowed bytes + for _, c := range results[i] { + if bytes.IndexByte([]byte(SecretBytesComplete), c) == -1 { + fmt.Printf("GenerateSecretWithComplexity(%d,%d,%d,%d,%d) returned invalid byte: %c", n, minlower, minupper, mindecimal, minspecial, c) + } + } + + // ensure each result is unique + for x := i; x > 0; x-- { + if bytes.Equal(results[x-1], results[i]) { + fmt.Printf("GenerateSecretWithComplexity(%d,%d,%d,%d,%d) returned two identical values: %s", n, minlower, minupper, mindecimal, minspecial, string(results[i])) + } + } + } + } + } + // simple one with no complexity constraint + test(10, 0, 0, 0, 0) + // test with complexity and different lengths + test(10, 1, 1, 1, 1) + test(24, 1, 1, 1, 1) + // test complexity that match exactly length + test(4, 1, 1, 1, 1) + test(8, 2, 2, 2, 2) +} + +func TestErrorGenerateSecretWithTooMuchComplexity(t *testing.T) { + testneg := func(n int, minlower int, minupper int, mindecimal int, minspecial int) { + var err error + _, err = GenerateSecretWithComplexity(n, minlower, minupper, mindecimal, minspecial) + fmt.Printf("err=%s ", err) + if err == nil { + fmt.Printf("GenerateSecretWithComplexity(%d,%d,%d,%d,%d) returned success when expected error about impossible complexity, err=%s", n, minlower, minupper, mindecimal, minspecial, err) + } + } + // test impossible combination ie complexity over length + testneg(4, 1, 1, 2, 1) + testneg(8, 2, 5, 1, 1) + testneg(24, 24, 1, 1, 1) +} + func TestSortContainerPorts(t *testing.T) { var ports []corev1.ContainerPort var want []corev1.ContainerPort diff --git a/pkg/splunk/util/secrets.go b/pkg/splunk/util/secrets.go index 35071e80a..91d44af25 100644 --- a/pkg/splunk/util/secrets.go +++ b/pkg/splunk/util/secrets.go @@ -459,8 +459,18 @@ func ApplyNamespaceScopedSecretObject(ctx context.Context, client splcommon.Cont // Value for token not found, generate if tokenType == "hec_token" { current.Data[tokenType] = generateHECToken() + } else if tokenType == "password" { + // use complexity for password + current.Data[tokenType], err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + return nil, err + } } else { - current.Data[tokenType] = splcommon.GenerateSecret(splcommon.SecretBytes, 24) + // disable complexity for secrets + current.Data[tokenType], err = splcommon.GenerateSecretWithComplexity(24, 0, 0, 0, 0) + if err != nil { + return nil, err + } } updateNeeded = true } @@ -488,8 +498,17 @@ func ApplyNamespaceScopedSecretObject(ctx context.Context, client splcommon.Cont for _, tokenType := range splcommon.GetSplunkSecretTokenTypes() { if tokenType == "hec_token" { current.Data[tokenType] = generateHECToken() + } else if tokenType == "password" { + // use complexity for password + current.Data[tokenType], err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + return nil, err + } } else { - current.Data[tokenType] = splcommon.GenerateSecret(splcommon.SecretBytes, 24) + current.Data[tokenType], err = splcommon.GenerateSecretWithComplexity(24, 0, 0, 0, 0) + if err != nil { + return nil, err + } } } diff --git a/pkg/splunk/util/secrets_test.go b/pkg/splunk/util/secrets_test.go index 9a9f50913..678823176 100644 --- a/pkg/splunk/util/secrets_test.go +++ b/pkg/splunk/util/secrets_test.go @@ -723,7 +723,10 @@ func TestGetLatestVersionedSecret(t *testing.T) { } // Update namespace scoped secret with new admin password - namespacescopedsecret.Data["password"] = splcommon.GenerateSecret(splcommon.SecretBytes, 24) + namespacescopedsecret.Data["password"], err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf(err.Error()) + } err = UpdateResource(context.TODO(), c, namespacescopedsecret) if err != nil { t.Errorf(err.Error()) @@ -798,7 +801,10 @@ func TestGetSplunkReadableNamespaceScopedSecretData(t *testing.T) { secretData := make(map[string][]byte) for _, tokenType := range splcommon.GetSplunkSecretTokenTypes() { if tokenType != "hec_token" { - secretData[tokenType] = splcommon.GenerateSecret(splcommon.SecretBytes, 24) + secretData[tokenType], err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf(err.Error()) + } } } @@ -943,6 +949,16 @@ func TestApplyNamespaceScopedSecretObject(t *testing.T) { // Partially baked "splunk-secrets" object(applies to empty as well) createCalls = map[string][]spltest.MockFuncCall{"Get": funcCalls, "Update": funcCalls} updateCalls = map[string][]spltest.MockFuncCall{"Get": funcCalls} + password, err := splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf("Error Generating Password With Complexity") + // FIXME : should we return here ? + } + pass4, err := splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf("Error Generating Password With Complexity") + // FIXME : should we return here ? + } secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -950,8 +966,8 @@ func TestApplyNamespaceScopedSecretObject(t *testing.T) { Namespace: "test", }, Data: map[string][]byte{ - "password": splcommon.GenerateSecret(splcommon.SecretBytes, 24), - "pass4Symmkey": splcommon.GenerateSecret(splcommon.SecretBytes, 24), + "password": password, + "pass4Symmkey": pass4, }, } spltest.ReconcileTester(t, "TestApplyNamespaceScopedSecretObject", "test", "test", createCalls, updateCalls, reconcile, false, &secret) @@ -959,6 +975,26 @@ func TestApplyNamespaceScopedSecretObject(t *testing.T) { // Fully baked splunk-secrets object createCalls = map[string][]spltest.MockFuncCall{"Get": funcCalls} updateCalls = map[string][]spltest.MockFuncCall{"Get": funcCalls} + password, err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf("Error Generating Password With Complexity") + // FIXME : should we return here ? + } + pass4, err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf("Error Generating Password With Complexity") + // FIXME : should we return here ? + } + idxc_secret, err := splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf("Error Generating Password With Complexity") + // FIXME : should we return here ? + } + shc_secret, err := splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1) + if err != nil { + t.Errorf("Error Generating Password With Complexity") + // FIXME : should we return here ? + } secret = corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -967,10 +1003,10 @@ func TestApplyNamespaceScopedSecretObject(t *testing.T) { }, Data: map[string][]byte{ "hec_token": generateHECToken(), - "password": splcommon.GenerateSecret(splcommon.SecretBytes, 24), - "pass4SymmKey": splcommon.GenerateSecret(splcommon.SecretBytes, 24), - "idxc_secret": splcommon.GenerateSecret(splcommon.SecretBytes, 24), - "shc_secret": splcommon.GenerateSecret(splcommon.SecretBytes, 24), + "password": password, + "pass4SymmKey": pass4, + "idxc_secret": idxc_secret, + "shc_secret": shc_secret, }, } spltest.ReconcileTester(t, "TestApplyNamespaceScopedSecretObject", "test", "test", createCalls, updateCalls, reconcile, false, &secret) @@ -985,7 +1021,7 @@ func TestApplyNamespaceScopedSecretObject(t *testing.T) { c.Create(ctx, &negSecret) rerr := errors.New(splcommon.Rerr) c.InduceErrorKind[splcommon.MockClientInduceErrorUpdate] = rerr - _, err := ApplyNamespaceScopedSecretObject(ctx, c, negSecret.GetNamespace()) + _, err = ApplyNamespaceScopedSecretObject(ctx, c, negSecret.GetNamespace()) if err == nil { t.Errorf("Expected error") }