CVE-2024-21495: Insufficient Information (NVD-CWE-noinfo)
value: Versions of the package github.com/greenpau/caddy-security before 1.0.42 are vulnerable to Insecure Randomness due to using an insecure random number generation library which could possibly be predicted via a brute-force search. Attackers could use the potentially predictable nonce value used for authentication purposes in the OAuth flow to conduct OAuth replay attacks. In addition, insecure randomness is used while generating multifactor authentication (MFA) secrets and creating API keys in the database package.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-02-17T05:15Z
Repository: https://github.com/greenpau/go-authcrunch
Language: Go
Files changed: Makefile, internal/tests/random.go, pkg/identity/api_key.go, pkg/identity/api_key_test.go, pkg/identity/database.go, pkg/identity/mfa_token.go, pkg/identity/public_key.go, pkg/util/random.go
Commit date: 2023-12-02 11:28:20 -0500
Commit message: security: addresses insecure randomness finding

See also: https://github.com/greenpau/caddy-security/issues/265

Diff: diff --git a/Makefile b/Makefile
index 4aa1e7b..2b1fffa 100644
--- a/Makefile
+++ b/Makefile
@@ -100,11 +100,13 @@ clean:
 
 .PHONY: qtest
 qtest: covdir
 	@echo "Perform quick tests ..."
 	@#time richgo test -v -coverprofile=.coverage/coverage.out internal/tag/*.go
+	@#time richgo test -v -coverprofile=.coverage/coverage.out internal/testutils/*.go
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/util/data/...
+	@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/util/...
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewConfig ./*.go
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewServer ./*.go
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/registry/...
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/waf/...
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestAuthorize ./pkg/authz/validator/...
@@ -145,11 +147,11 @@ qtest: covdir
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestMatchAclRuleCondition ./pkg/acl/...
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/user/...
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authproxy/...
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/identity/...
 	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authn/backends/...
-	@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run NewAPIKey ./pkg/identity/...
+	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run NewAPIKey ./pkg/identity/...
 	@go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html
 	@#go tool cover -func=.coverage/coverage.out | grep -v "100.0"
 	@go tool cover -func=.coverage/coverage.out
 	@echo "$@: complete"
 
diff --git a/internal/tests/random.go b/internal/tests/random.go
index 797615e..69b2551 100644
--- a/internal/tests/random.go
+++ b/internal/tests/random.go
@@ -13,12 +13,14 @@
 // limitations under the License.
 
 package tests
 
 import (
+	"crypto/rand"
 	"github.com/google/uuid"
-	"math/rand"
+	"io"
+	mathrand "math/rand"
 	"strings"
 )
 
 // NewID returns a random ID to be used for user identification.
 func NewID() string {
@@ -26,14 +28,26 @@ func NewID() string {
 }
 
 // NewRandomString returns a random string.
 func NewRandomString(length int) string {
 	chars := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
+	charsLen := byte(36)
+
 	if length == 0 {
 		length = 32
 	}
-	var b strings.Builder
-	for i := 0; i < length; i++ {
-		b.WriteRune(chars[rand.Intn(len(chars))])
+
+	b := make([]byte, length)
+	if _, err := io.ReadFull(rand.Reader, b); err != nil {
+		var sb strings.Builder
+		for i := 0; i < length; i++ {
+			sb.WriteRune(chars[mathrand.Intn(len(chars))])
+		}
+		return sb.String()
 	}
-	return b.String()
+
+	for i, char := range b {
+		b[i] = byte(chars[char%charsLen])
+	}
+
+	return string(b)
 }
diff --git a/pkg/identity/api_key.go b/pkg/identity/api_key.go
index f96bd59..aba2023 100644
--- a/pkg/identity/api_key.go
+++ b/pkg/identity/api_key.go
@@ -15,10 +15,11 @@
 package identity
 
 import (
 	"github.com/greenpau/go-authcrunch/pkg/errors"
 	"github.com/greenpau/go-authcrunch/pkg/requests"
+	"github.com/greenpau/go-authcrunch/pkg/util"
 	"golang.org/x/crypto/bcrypt"
 	"time"
 )
 
 // APIKeyBundle is a collection of API keys.
@@ -78,11 +79,11 @@ func NewAPIKey(r *requests.Request) (*APIKey, error) {
 	if r.Key.Comment == "" {
 		return nil, errors.ErrAPIKeyCommentEmpty
 	}
 	p := &APIKey{
 		Comment:   r.Key.Comment,
-		ID:        GetRandomString(40),
+		ID:        util.GetRandomString(40),
 		Prefix:    r.Key.Prefix,
 		Payload:   r.Key.Payload,
 		Usage:     r.Key.Usage,
 		CreatedAt: time.Now().UTC(),
 	}
diff --git a/pkg/identity/api_key_test.go b/pkg/identity/api_key_test.go
index 0934aae..f95cab9 100644
--- a/pkg/identity/api_key_test.go
+++ b/pkg/identity/api_key_test.go
@@ -19,10 +19,11 @@ import (
 	"testing"
 
 	"github.com/greenpau/go-authcrunch/internal/tests"
 	"github.com/greenpau/go-authcrunch/pkg/errors"
 	"github.com/greenpau/go-authcrunch/pkg/requests"
+	"github.com/greenpau/go-authcrunch/pkg/util"
 )
 
 func TestNewAPIKey(t *testing.T) {
 	testcases := []struct {
 		name      string
@@ -35,11 +36,11 @@ func TestNewAPIKey(t *testing.T) {
 			name: "test api key",
 			req: &requests.Request{
 				Key: requests.Key{
 					Usage:   "api",
 					Comment: "jsmith-api-key",
-					Payload: GetRandomStringFromRange(54, 72),
+					Payload: util.GetRandomStringFromRange(54, 72),
 				},
 			},
 			want: map[string]interface{}{
 				"usage":    "api",
 				"comment":  "jsmith-api-key",
@@ -51,11 +52,11 @@ func TestNewAPIKey(t *testing.T) {
 			req: &requests.Request{
 				Key: requests.Key{
 					Usage:    "api",
 					Comment:  "jsmith-api-key",
 					Disabled: true,
-					Payload:  GetRandomStringFromRange(54, 72),
+					Payload:  util.GetRandomStringFromRange(54, 72),
 				},
 			},
 			want: map[string]interface{}{
 				"usage":    "api",
 				"comment":  "jsmith-api-key",
@@ -89,11 +90,11 @@ func TestNewAPIKey(t *testing.T) {
 		{
 			name: "test api key with empty usage",
 			req: &requests.Request{
 				Key: requests.Key{
 					Comment:  "jsmith-api-key",
-					Payload:  GetRandomStringFromRange(54, 72),
+					Payload:  util.GetRandomStringFromRange(54, 72),
 					Disabled: true,
 				},
 			},
 			shouldErr: true,
 			err:       errors.ErrAPIKeyUsageEmpty,
@@ -102,11 +103,11 @@ func TestNewAPIKey(t *testing.T) {
 			name: "test api key with unsupported usage",
 			req: &requests.Request{
 				Key: requests.Key{
 					Usage:    "foo",
 					Comment:  "jsmith-api-key",
-					Payload:  GetRandomStringFromRange(54, 72),
+					Payload:  util.GetRandomStringFromRange(54, 72),
 					Disabled: true,
 				},
 			},
 			shouldErr: true,
 			err:       errors.ErrAPIKeyUsageUnsupported.WithArgs("foo"),
@@ -114,11 +115,11 @@ func TestNewAPIKey(t *testing.T) {
 		{
 			name: "test api key with empty comment",
 			req: &requests.Request{
 				Key: requests.Key{
 					Usage:    "api",
-					Payload:  GetRandomStringFromRange(54, 72),
+					Payload:  util.GetRandomStringFromRange(54, 72),
 					Disabled: true,
 				},
 			},
 			shouldErr: true,
 			err:       errors.ErrAPIKeyCommentEmpty,
diff --git a/pkg/identity/database.go b/pkg/identity/database.go
index 184fa58..6f35c89 100644
--- a/pkg/identity/database.go
+++ b/pkg/identity/database.go
@@ -18,10 +18,11 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/greenpau/go-authcrunch/internal/utils"
 	"github.com/greenpau/go-authcrunch/pkg/errors"
 	"github.com/greenpau/go-authcrunch/pkg/requests"
+	"github.com/greenpau/go-authcrunch/pkg/util"
 	"github.com/greenpau/versioned"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -564,11 +565,11 @@ func (db *Database) AddAPIKey(r *requests.Request) error {
 	defer db.mu.Unlock()
 	user, err := db.validateUserIdentity(r.User.Username, r.User.Email)
 	if err != nil {
 		return errors.ErrAddAPIKey.WithArgs(r.Key.Usage, err)
 	}
-	s := GetRandomStringFromRange(72, 96)
+	s := util.GetRandomStringFromRange(72, 96)
 	failCount := 0
 	for {
 		hk, err := NewPassword(s)
 		if err != nil {
 			if failCount > 10 {
diff --git a/pkg/identity/mfa_token.go b/pkg/identity/mfa_token.go
index 1780421..f2c1114 100644
--- a/pkg/identity/mfa_token.go
+++ b/pkg/identity/mfa_token.go
@@ -34,10 +34,11 @@ import (
 	"strings"
 	"time"
 
 	"github.com/greenpau/go-authcrunch/pkg/errors"
 	"github.com/greenpau/go-authcrunch/pkg/requests"
+	"github.com/greenpau/go-authcrunch/pkg/util"
 )
 
 // MfaTokenBundle is a collection of public keys.
 type MfaTokenBundle struct {
 	tokens []*MfaToken
@@ -97,11 +98,11 @@ func (b *MfaTokenBundle) Size() int {
 }
 
 // NewMfaToken returns an instance of MfaToken.
 func NewMfaToken(req *requests.Request) (*MfaToken, error) {
 	p := &MfaToken{
-		ID:         GetRandomString(40),
+		ID:         util.GetRandomString(40),
 		CreatedAt:  time.Now().UTC(),
 		Parameters: make(map[string]string),
 		Flags:      make(map[string]bool),
 		Comment:    req.MfaToken.Comment,
 		Type:       req.MfaToken.Type,
diff --git a/pkg/identity/public_key.go b/pkg/identity/public_key.go
index 71d434c..64a5c36 100644
--- a/pkg/identity/public_key.go
+++ b/pkg/identity/public_key.go
@@ -22,10 +22,11 @@ import (
 	"encoding/hex"
 	"encoding/pem"
 	"fmt"
 	"github.com/greenpau/go-authcrunch/pkg/errors"
 	"github.com/greenpau/go-authcrunch/pkg/requests"
+	"github.com/greenpau/go-authcrunch/pkg/util"
 	"golang.org/x/crypto/openpgp"
 	"golang.org/x/crypto/ssh"
 	"strconv"
 	"strings"
 	"time"
@@ -85,11 +86,11 @@ func (b *PublicKeyBundle) Size() int {
 
 // NewPublicKey returns an instance of PublicKey.
 func NewPublicKey(r *requests.Request) (*PublicKey, error) {
 	p := &PublicKey{
 		Comment:   r.Key.Comment,
-		ID:        GetRandomString(40),
+		ID:        util.GetRandomString(40),
 		Payload:   r.Key.Payload,
 		Usage:     r.Key.Usage,
 		CreatedAt: time.Now().UTC(),
 	}
 	if err := p.parse(); err != nil {
diff --git a/pkg/identity/random.go b/pkg/identity/random.go
deleted file mode 100644
index 53b5b97..0000000
--- a/pkg/identity/random.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2022 Paul Greenberg greenpau@outlook.com
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package identity
-
-import (
-	"math/rand"
-	"time"
-)
-
-const charset = "abcdefghijklmnopqrstuvwxyz" +
-	"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-
-var seed *rand.Rand = rand.New(
-	rand.NewSource(time.Now().UnixNano()),
-)
-
-func gen(length int, charset string) string {
-	b := make([]byte, length)
-	for i := range b {
-		b[i] = charset[seed.Intn(len(charset))]
-	}
-	return string(b)
-}
-
-// GetRandomString returns X character long random string.
-func GetRandomString(i int) string {
-	if i < 1 {
-		i = 40
-	}
-	return gen(i, charset)
-}
-
-// GetRandomStringFromRange generates random string of a random length. The
-// random lenght is bounded by a and b.
-func GetRandomStringFromRange(a, b int) string {
-	var i int
-	if a > b {
-		i = rand.Intn(a-b) + b
-	} else {
-		i = rand.Intn(b-a) + a
-	}
-	return gen(i, charset)
-}
diff --git a/pkg/util/random.go b/pkg/util/random.go
index 0bc25a6..4131839 100644
--- a/pkg/util/random.go
+++ b/pkg/util/random.go
@@ -13,13 +13,16 @@
 // limitations under the License.
 
 package util
 
 import (
+	"crypto/rand"
 	"encoding/base32"
-	"math/rand"
-	"time"
+	"io"
+	//	"math"
+	"math/big"
+	mathrand "math/rand"
 	"unicode"
 )
 
 const charset = "abcdefghijklmnopqrstuvwxyz" +
 	"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
@@ -31,38 +34,55 @@ var charsetTable = &unicode.RangeTable{
 		{0x0030, 0x0039, 1}, // 0-9, where 0 is hex 30
 	},
 	LatinOffset: 1,
 }
 
-var seed *rand.Rand = rand.New(
-	rand.NewSource(time.Now().UnixNano()),
-)
+func genRandInt(i int) uint32 {
+	n, err := rand.Int(rand.Reader, big.NewInt(int64(i)))
+	if err != nil {
+		return uint32(mathrand.Intn(i))
+	}
+	//if n.Uint64() > math.MaxUint32+1 {
+	//	return uint32(n.Uint64() & uint32(0xFFFFFFFF))
+	//}
+	return uint32(n.Uint64())
+}
 
-func gen(length int, charset string) string {
+func gen(length uint32, charset string) string {
+	charsetLen := byte(len(charset))
 	b := make([]byte, length)
-	for i := range b {
-		b[i] = charset[seed.Intn(len(charset))]
+	if _, err := io.ReadFull(rand.Reader, b); err != nil {
+		// for i uint32 := 0; i < length; i++ {
+		for i := uint32(0); i < length; {
+			b[i] = charset[mathrand.Intn(len(charset))]
+		}
+		return string(b)
 	}
+
+	for i, char := range b {
+		b[i] = byte(charset[char%charsetLen])
+	}
+
 	return string(b)
 }
 
 // GetRandomString returns X character long random string.
 func GetRandomString(i int) string {
 	if i < 1 {
 		i = 40
 	}
-	return gen(i, charset)
+	return gen(uint32(i), charset)
 }
 
 // GetRandomStringFromRange generates random string of a random length. The
 // random lenght is bounded by a and b.
 func GetRandomStringFromRange(a, b int) string {
-	var i int
+	var i uint32
 	if a > b {
-		i = rand.Intn(a-b) + b
+		i = genRandInt(a-b) + uint32(b)
 	} else {
-		i = rand.Intn(b-a) + a
+		i = genRandInt(b-a) + uint32(a)
 	}
 	return gen(i, charset)
 }
 
 // GetRandomEncodedStringFromRange return the number returned by
@@ -73,15 +93,15 @@ func GetRandomEncodedStringFromRange(a, b int) string {
 }
 
 // GetRandomStringFromRangeWithCharset generates random string of a random length. The
 // random lenght is bounded by a and b. The charset is provided.
 func GetRandomStringFromRangeWithCharset(a, b int, cs string) string {
-	var i int
+	var i uint32
 	if a > b {
-		i = rand.Intn(a-b) + b
+		i = genRandInt(a-b) + uint32(b)
 	} else {
-		i = rand.Intn(b-a) + a
+		i = genRandInt(b-a) + uint32(a)
 	}
 	return gen(i, cs)
 }
 
 // IsMalformedRandomString checks whether the provided string was generated by

Vulnerable code: 	@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run NewAPIKey ./pkg/identity/...
	"math/rand"
	var b strings.Builder
	for i := 0; i < length; i++ {
		b.WriteRune(chars[rand.Intn(len(chars))])
	return b.String()
		ID:        GetRandomString(40),
					Payload: GetRandomStringFromRange(54, 72),
					Payload:  GetRandomStringFromRange(54, 72),
					Payload:  GetRandomStringFromRange(54, 72),
					Payload:  GetRandomStringFromRange(54, 72),
					Payload:  GetRandomStringFromRange(54, 72),
	s := GetRandomStringFromRange(72, 96)
		ID:         GetRandomString(40),
		ID:        GetRandomString(40),
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package identity

import (
	"math/rand"
	"time"
)

const charset = "abcdefghijklmnopqrstuvwxyz" +
	"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

var seed *rand.Rand = rand.New(
	rand.NewSource(time.Now().UnixNano()),
)

func gen(length int, charset string) string {
	b := make([]byte, length)
	for i := range b {
		b[i] = charset[seed.Intn(len(charset))]
	}
	return string(b)
}

// GetRandomString returns X character long random string.
func GetRandomString(i int) string {
	if i < 1 {
		i = 40
	}
	return gen(i, charset)
}

// GetRandomStringFromRange generates random string of a random length. The
// random lenght is bounded by a and b.
func GetRandomStringFromRange(a, b int) string {
	var i int
	if a > b {
		i = rand.Intn(a-b) + b
	} else {
		i = rand.Intn(b-a) + a
	}
	return gen(i, charset)
}
	"math/rand"
	"time"
var seed *rand.Rand = rand.New(
	rand.NewSource(time.Now().UnixNano()),
)
func gen(length int, charset string) string {
	for i := range b {
		b[i] = charset[seed.Intn(len(charset))]
	return gen(i, charset)
	var i int
		i = rand.Intn(a-b) + b
		i = rand.Intn(b-a) + a
	var i int
		i = rand.Intn(a-b) + b
		i = rand.Intn(b-a) + a
Fixed code: 	@#time richgo test -v -coverprofile=.coverage/coverage.out internal/testutils/*.go
	@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/util/...
	@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run NewAPIKey ./pkg/identity/...
	"crypto/rand"
	"io"
	mathrand "math/rand"
	charsLen := byte(36)


	b := make([]byte, length)
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		var sb strings.Builder
		for i := 0; i < length; i++ {
			sb.WriteRune(chars[mathrand.Intn(len(chars))])
		}
		return sb.String()

	for i, char := range b {
		b[i] = byte(chars[char%charsLen])
	}

	return string(b)
	"github.com/greenpau/go-authcrunch/pkg/util"
		ID:        util.GetRandomString(40),
	"github.com/greenpau/go-authcrunch/pkg/util"
					Payload: util.GetRandomStringFromRange(54, 72),
					Payload:  util.GetRandomStringFromRange(54, 72),
					Payload:  util.GetRandomStringFromRange(54, 72),
					Payload:  util.GetRandomStringFromRange(54, 72),
					Payload:  util.GetRandomStringFromRange(54, 72),
	"github.com/greenpau/go-authcrunch/pkg/util"
	s := util.GetRandomStringFromRange(72, 96)
	"github.com/greenpau/go-authcrunch/pkg/util"
		ID:         util.GetRandomString(40),
	"github.com/greenpau/go-authcrunch/pkg/util"
		ID:        util.GetRandomString(40),
	"crypto/rand"
	"io"
	//	"math"
	"math/big"
	mathrand "math/rand"
func genRandInt(i int) uint32 {
	n, err := rand.Int(rand.Reader, big.NewInt(int64(i)))
	if err != nil {
		return uint32(mathrand.Intn(i))
	}
	//if n.Uint64() > math.MaxUint32+1 {
	//	return uint32(n.Uint64() & uint32(0xFFFFFFFF))
	//}
	return uint32(n.Uint64())
}
func gen(length uint32, charset string) string {
	charsetLen := byte(len(charset))
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		// for i uint32 := 0; i < length; i++ {
		for i := uint32(0); i < length; {
			b[i] = charset[mathrand.Intn(len(charset))]
		}
		return string(b)

	for i, char := range b {
		b[i] = byte(charset[char%charsetLen])
	}

	return gen(uint32(i), charset)
	var i uint32
		i = genRandInt(a-b) + uint32(b)
		i = genRandInt(b-a) + uint32(a)
	var i uint32
		i = genRandInt(a-b) + uint32(b)
		i = genRandInt(b-a) + uint32(a)