Skip to content

GODRIVER-2349 Seed all pseudorandom number generators with a crypto-secure random number. #889

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

Merged
merged 4 commits into from
Mar 30, 2022
Merged
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
23 changes: 23 additions & 0 deletions internal/randutil/randutil.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// 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

// Package randutil provides common random number utilities.
package randutil

import (
crand "crypto/rand"
"fmt"
"io"
"math/rand"
"sync"
)
Expand Down Expand Up @@ -52,3 +61,17 @@ func (lr *LockedRand) Shuffle(n int, swap func(i, j int)) {
lr.r.Shuffle(n, swap)
lr.mu.Unlock()
}

// CryptoSeed returns a random int64 read from the "crypto/rand" random number generator. It is
// intended to be used to seed pseudorandom number generators at package initialization. It panics
// if it encounters any errors.
func CryptoSeed() int64 {
var b [8]byte
_, err := io.ReadFull(crand.Reader, b[:])
if err != nil {
panic(fmt.Errorf("failed to read 8 bytes from a \"crypto/rand\".Reader: %v", err))
}

return (int64(b[0]) << 0) | (int64(b[1]) << 8) | (int64(b[2]) << 16) | (int64(b[3]) << 24) |
(int64(b[4]) << 32) | (int64(b[5]) << 40) | (int64(b[6]) << 48) | (int64(b[7]) << 56)
}
22 changes: 22 additions & 0 deletions internal/randutil/randutil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// 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

package randutil

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestCryptoSeed(t *testing.T) {
seeds := make(map[int64]bool)
for i := 1; i < 1000000; i++ {
s := CryptoSeed()
require.False(t, seeds[s], "CryptoSeed returned a duplicate value %d", s)
seeds[s] = true
}
}
2 changes: 1 addition & 1 deletion x/mongo/driver/connstring/connstring.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
)

// random is a package-global pseudo-random number generator.
var random = randutil.NewLockedRand(rand.NewSource(time.Now().UnixNano()))
var random = randutil.NewLockedRand(rand.NewSource(randutil.CryptoSeed()))

// ParseAndValidate parses the provided URI into a ConnString object.
// It check that all values are valid.
Expand Down
2 changes: 1 addition & 1 deletion x/mongo/driver/topology/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ var ErrServerSelectionTimeout = errors.New("server selection timeout")
type MonitorMode uint8

// random is a package-global pseudo-random number generator.
var random = randutil.NewLockedRand(rand.NewSource(time.Now().UnixNano()))
var random = randutil.NewLockedRand(rand.NewSource(randutil.CryptoSeed()))

// These constants are the available monitoring modes.
const (
Expand Down
37 changes: 28 additions & 9 deletions x/mongo/driver/uuid/uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,24 @@ package uuid // import "go.mongodb.org/mongo-driver/x/mongo/driver/uuid"
import (
"io"
"math/rand"
"time"

"go.mongodb.org/mongo-driver/internal/randutil"
)

// UUID represents a UUID.
type UUID [16]byte

// random is a package-global pseudo-random number generator.
var random = randutil.NewLockedRand(rand.NewSource(time.Now().UnixNano()))
// A source is a UUID generator that reads random values from a randutil.LockedRand.
// It is safe to use from multiple goroutines.
type source struct {
random *randutil.LockedRand
}

// New returns a random UUIDv4. It uses a "math/rand" pseudo-random number generator seeded with the
// package initialization time.
//
// New should not be used to generate cryptographically-secure random UUIDs.
func New() (UUID, error) {
// new returns a random UUIDv4 with bytes read from the source's random number generator.
func (s *source) new() (UUID, error) {
var uuid [16]byte

_, err := io.ReadFull(random, uuid[:])
_, err := io.ReadFull(s.random, uuid[:])
if err != nil {
return [16]byte{}, err
}
Expand All @@ -37,6 +36,26 @@ func New() (UUID, error) {
return uuid, nil
}

// newGlobalSource returns a source that uses a "math/rand" pseudo-random number generator seeded
// with a cryptographically-secure random number. It is intended to be used to initialize the
// package-global UUID generator.
func newGlobalSource() *source {
return &source{
random: randutil.NewLockedRand(rand.NewSource(randutil.CryptoSeed())),
}
}

// globalSource is a package-global pseudo-random UUID generator.
var globalSource = newGlobalSource()

// New returns a random UUIDv4. It uses a "math/rand" pseudo-random number generator seeded with a
// cryptographically-secure random number at package initialization.
//
// New should not be used to generate cryptographically-secure random UUIDs.
func New() (UUID, error) {
return globalSource.new()
}

// Equal returns true if two UUIDs are equal.
func Equal(a, b UUID) bool {
return a == b
Expand Down
47 changes: 43 additions & 4 deletions x/mongo/driver/uuid/uuid_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,56 @@
// Copyright (C) MongoDB, Inc. 2022-present.
//
// 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

package uuid

import (
"sync"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNew(t *testing.T) {
m := make(map[UUID]bool)
for i := 1; i < 100; i++ {
for i := 1; i < 1000000; i++ {
uuid, err := New()
assert.NoError(t, err, "New error")
assert.False(t, m[uuid], "New returned a duplicate UUID %v", uuid)
require.NoError(t, err, "New error")
require.False(t, m[uuid], "New returned a duplicate UUID %v", uuid)
m[uuid] = true
}
}

// GODRIVER-2349
// Test that initializing many package-global UUID sources concurrently never leads to any duplicate
// UUIDs being generated.
func TestGlobalSource(t *testing.T) {
// Create a slice of 1,000 sources and initialize them in 1,000 separate goroutines. The goal is
// to emulate many separate Go driver processes starting at the same time and initializing the
// uuid package at the same time.
sources := make([]*source, 1000)
var wg sync.WaitGroup
for i := range sources {
wg.Add(1)
go func(i int) {
defer wg.Done()
sources[i] = newGlobalSource()
}(i)
}
wg.Wait()

// Read 1,000 UUIDs from each source and assert that there is never a duplicate value, either
// from the same source or from separate sources.
const iterations = 1000
uuids := make(map[UUID]bool, len(sources)*iterations)
for i := 0; i < iterations; i++ {
for j, s := range sources {
uuid, err := s.new()
require.NoError(t, err, "new() error")
require.Falsef(t, uuids[uuid], "source %d returned a duplicate UUID on iteration %d: %v", j, i, uuid)
uuids[uuid] = true
}
}
}