Skip to content

Feature/secrets with complexity #1510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/splunk/common/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ 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-*&%#@,.;:/?[]{}+=-_<>"

// HexBytes used to generate random hexadecimal strings (e.g. HEC tokens)
HexBytes = "ABCDEF01234567890"

Expand Down
97 changes: 89 additions & 8 deletions pkg/splunk/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,19 @@ package common

import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"math/rand"
"os"
"reflect"
"sort"
"strings"
"time"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func init() {
// seed random number generator for splunk secret generation
rand.Seed(time.Now().UnixNano())
}

// AsOwner returns an object to use for Kubernetes resource ownership references.
func AsOwner(cr MetaObject, isController bool) metav1.OwnerReference {
return metav1.OwnerReference{
Expand Down Expand Up @@ -98,11 +92,98 @@ 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 {
//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
}
Expand Down
61 changes: 61 additions & 0 deletions pkg/splunk/common/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions pkg/splunk/util/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,10 @@ func ApplyNamespaceScopedSecretObject(ctx context.Context, client splcommon.Cont
if tokenType == "hec_token" {
current.Data[tokenType] = generateHECToken()
} else {
current.Data[tokenType] = splcommon.GenerateSecret(splcommon.SecretBytes, 24)
current.Data[tokenType], err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1)
if err != nil {
return nil, err
}
}
updateNeeded = true
}
Expand Down Expand Up @@ -489,7 +492,10 @@ func ApplyNamespaceScopedSecretObject(ctx context.Context, client splcommon.Cont
if tokenType == "hec_token" {
current.Data[tokenType] = generateHECToken()
} else {
current.Data[tokenType] = splcommon.GenerateSecret(splcommon.SecretBytes, 24)
current.Data[tokenType], err = splcommon.GenerateSecretWithComplexity(24, 1, 1, 1, 1)
if err != nil {
return nil, err
}
}
}

Expand Down
56 changes: 46 additions & 10 deletions pkg/splunk/util/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,11 @@ 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())
Expand Down Expand Up @@ -798,7 +802,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())
}
}
}

Expand Down Expand Up @@ -943,34 +950,63 @@ 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{
Name: splcommon.GetNamespaceScopedSecretName("test"),
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)

// 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{
Name: splcommon.GetNamespaceScopedSecretName("test"),
Namespace: "test",
},
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)
Expand All @@ -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")
}
Expand Down
Loading