diff --git a/uuid.go b/uuid.go index dc75cee..4bcb226 100644 --- a/uuid.go +++ b/uuid.go @@ -6,11 +6,15 @@ package uuid import ( "bytes" - "crypto/rand" "encoding/hex" "errors" "fmt" "io" + + // Uses math/rand/v2 for random number generation + // because it uses chacha8 as the default source, + // which is cryptographically secure and much faster + "strings" "sync" ) @@ -42,8 +46,8 @@ const Standard = RFC4122 const randPoolSize = 16 * 16 var ( - rander = rand.Reader // random function - poolEnabled = false + rander io.Reader = defaultRandReader{} + poolEnabled = false poolMu sync.Mutex poolPos = randPoolSize // protected with poolMu pool [randPoolSize]byte // protected with poolMu @@ -52,7 +56,7 @@ var ( ErrInvalidBracketedFormat = errors.New("invalid bracketed UUID format") ) -type URNPrefixError struct { prefix string } +type URNPrefixError struct{ prefix string } func (e URNPrefixError) Error() string { return fmt.Sprintf("invalid urn prefix: %q", e.prefix) @@ -215,10 +219,12 @@ func Must(uuid UUID, err error) UUID { } // Validate returns an error if s is not a properly formatted UUID in one of the following formats: -// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +// +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +// // It returns an error if the format is invalid, otherwise nil. func Validate(s string) error { switch len(s) { @@ -346,7 +352,7 @@ func (v Variant) String() string { // generator. func SetRand(r io.Reader) { if r == nil { - rander = rand.Reader + rander = defaultRandReader{} return } rander = r diff --git a/version4.go b/version4.go index 7697802..fa25d1c 100644 --- a/version4.go +++ b/version4.go @@ -4,12 +4,16 @@ package uuid -import "io" +import ( + "encoding/binary" + "io" + chacha8RandV2 "math/rand/v2" +) // New creates a new random UUID or panics. New is equivalent to // the expression // -// uuid.Must(uuid.NewRandom()) +// uuid.Must(uuid.NewRandom()) func New() UUID { return Must(NewRandom()) } @@ -17,7 +21,7 @@ func New() UUID { // NewString creates a new random UUID and returns it as a string or panics. // NewString is equivalent to the expression // -// uuid.New().String() +// uuid.New().String() func NewString() string { return Must(NewRandom()).String() } @@ -31,12 +35,16 @@ func NewString() string { // // A note about uniqueness derived from the UUID Wikipedia entry: // -// Randomly generated UUIDs have 122 random bits. One's annual risk of being -// hit by a meteorite is estimated to be one chance in 17 billion, that -// means the probability is about 0.00000000006 (6 × 10−11), -// equivalent to the odds of creating a few tens of trillions of UUIDs in a -// year and having one duplicate. +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. func NewRandom() (UUID, error) { + if _, ok := rander.(defaultRandReader); ok { + return fastRandV4DefaultZeroAlloc(), nil + } + if !poolEnabled { return NewRandomFromReader(rander) } @@ -74,3 +82,36 @@ func newRandomFromPool() (UUID, error) { uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 return uuid, nil } + +// Singleton with no state that implements +// io.Reader and uses the default rand/v2 +// package to read bytes. +type defaultRandReader struct{} + +// Read fills the provided byte slice `b` with random data using a +// cryptographically secure random number generator. +func (d defaultRandReader) Read(b []byte) (n int, err error) { + var num uint64 + numByteIndex := 0 + + for i := range b { + if numByteIndex == 0 { + num = chacha8RandV2.Uint64() + } + b[i] = byte(num >> (numByteIndex * 8)) + numByteIndex = (numByteIndex + 1) % 8 + } + + return len(b), nil +} + +func fastRandV4DefaultZeroAlloc() UUID { + var uuid UUID + hi, low := chacha8RandV2.Uint64(), chacha8RandV2.Uint64() + binary.BigEndian.PutUint64(uuid[0:8], hi) + binary.BigEndian.PutUint64(uuid[8:16], low) + + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid +}