This commit is contained in:
binwiederhier
2026-06-12 11:07:48 -04:00
parent d1696ac5b6
commit fd716e4807
12 changed files with 816 additions and 23 deletions
+26 -10
View File
@@ -2,20 +2,19 @@ package util
import (
"bytes"
crand "crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"math/rand"
"net/netip"
"os"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
@@ -30,8 +29,6 @@ const (
)
var (
random = rand.New(rand.NewSource(time.Now().UnixNano()))
randomMutex = sync.Mutex{}
sizeStrRegex = regexp.MustCompile(`(?i)^(\d+)([gmkb])?$`)
errInvalidPriority = errors.New("invalid priority")
noQuotesRegex = regexp.MustCompile(`^[-_./:@a-zA-Z0-9]+$`)
@@ -144,14 +141,33 @@ func RandomLowerStringPrefix(prefix string, length int) string {
return randomStringPrefixWithCharset(prefix, length, randomStringLowerCaseCharset)
}
// randomStringPrefixWithCharset builds a random string from charset using crypto/rand.
// We use rejection sampling (dropping the few highest byte values that would skew the
// distribution) so every character is uniformly distributed -- important because these
// strings back security tokens (access tokens, magic-link tokens, IDs), not just labels.
func randomStringPrefixWithCharset(prefix string, length int, charset string) string {
randomMutex.Lock() // Who would have thought that random.Intn() is not thread-safe?!
defer randomMutex.Unlock()
b := make([]byte, length-len(prefix))
for i := range b {
b[i] = charset[random.Intn(len(charset))]
n := length - len(prefix)
if n <= 0 {
return prefix[:length]
}
return prefix + string(b)
result := make([]byte, n)
limit := 256 - (256 % len(charset)) // reject byte values >= limit to avoid modulo bias
buf := make([]byte, n)
for i := 0; i < n; {
if _, err := crand.Read(buf); err != nil {
panic("crypto/rand failed: " + err.Error()) // Should never happen on a sane system
}
for _, c := range buf {
if i >= n {
break
}
if int(c) < limit {
result[i] = charset[int(c)%len(charset)]
i++
}
}
}
return prefix + string(result)
}
// ValidRandomString returns true if the given string matches the format created by RandomString
+24
View File
@@ -25,6 +25,30 @@ func TestRandomString(t *testing.T) {
require.NotEqual(t, s1, s2)
}
// TestRandomString_CSPRNG guards the crypto/rand-backed generator: every character must come
// from the expected charset (rejection sampling correctness) and a large batch must be unique
// (no clock-seeded PRNG collapsing to a predictable stream).
func TestRandomString_CSPRNG(t *testing.T) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
seen := make(map[string]bool)
charCounts := make(map[rune]int)
for i := 0; i < 5000; i++ {
s := RandomString(48)
require.Equal(t, 48, len(s))
require.False(t, seen[s], "duplicate random string generated")
seen[s] = true
for _, c := range s {
require.Contains(t, charset, string(c))
charCounts[c]++
}
}
// Every charset character should appear at least once across 5000*48 draws; a heavily
// biased or broken generator would leave gaps.
for _, c := range charset {
require.Greater(t, charCounts[c], 0, "character %q never appeared", string(c))
}
}
func TestFileExists(t *testing.T) {
filename := filepath.Join(t.TempDir(), "somefile.txt")
require.Nil(t, os.WriteFile(filename, []byte{0x25, 0x86}, 0600))