From 4937903190eba0aac75b1065b8dcce7cab502aaa Mon Sep 17 00:00:00 2001 From: clawbot Date: Sat, 5 Sep 2026 03:28:27 +0000 Subject: [PATCH] add RandomHexString for unguessable identifiers and tokens RandomHexString returns the requested number of bytes from crypto/rand as lowercase hexadecimal, so callers reaching for a token do not end up using the predictable math/rand instead. Comes with a doc comment and table-driven tests. (closes #13) Model: opus-5 --- random.go | 28 +++++++++++++++++++++++++ random_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 random.go create mode 100644 random_test.go diff --git a/random.go b/random.go new file mode 100644 index 0000000..4ed4540 --- /dev/null +++ b/random.go @@ -0,0 +1,28 @@ +package util + +import ( + "crypto/rand" + "encoding/hex" + "errors" +) + +// RandomHexString returns byteLength random bytes from the operating system's +// random source, written as lowercase hexadecimal, so the returned string is +// twice as long as byteLength. The bytes come from crypto/rand, which means the +// result is fit for session tokens, temporary filenames and anything else a +// stranger should not be able to guess. +// +// An error comes back only for a negative length or if the random source +// itself fails, which does not happen on a working system. +func RandomHexString(byteLength int) (string, error) { + if byteLength < 0 { + return "", errors.New("byte length cannot be negative") + } + + buffer := make([]byte, byteLength) + if _, err := rand.Read(buffer); err != nil { + return "", err + } + + return hex.EncodeToString(buffer), nil +} diff --git a/random_test.go b/random_test.go new file mode 100644 index 0000000..9e24f66 --- /dev/null +++ b/random_test.go @@ -0,0 +1,57 @@ +package util + +import ( + "encoding/hex" + "testing" +) + +func TestRandomHexStringLength(t *testing.T) { + tests := []struct { + name string + byteLength int + expectedLength int + }{ + {"no bytes at all", 0, 0}, + {"one byte", 1, 2}, + {"eight bytes", 8, 16}, + {"sixteen bytes", 16, 32}, + {"thirty-two bytes", 32, 64}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := RandomHexString(test.byteLength) + if err != nil { + t.Fatalf("did not expect an error, got %v", err) + } + if len(got) != test.expectedLength { + t.Errorf("expected a string of %d characters, got %d (%q)", test.expectedLength, len(got), got) + } + if _, err := hex.DecodeString(got); err != nil { + t.Errorf("expected valid hexadecimal, got %q: %v", got, err) + } + }) + } +} + +func TestRandomHexStringRejectsNegativeLength(t *testing.T) { + got, err := RandomHexString(-1) + if err == nil { + t.Errorf("expected an error for a negative length, got %q", got) + } +} + +func TestRandomHexStringDiffersBetweenCalls(t *testing.T) { + seen := make(map[string]struct{}) + + for i := 0; i < 100; i++ { + value, err := RandomHexString(16) + if err != nil { + t.Fatalf("did not expect an error, got %v", err) + } + if _, repeated := seen[value]; repeated { + t.Fatalf("got the same string twice: %q", value) + } + seen[value] = struct{}{} + } +}