فهرست منبع

Use SipHash-based PRNG for image/audio variations.

Dmitry Chestnykh 12 سال پیش
والد
کامیت
26f056818c
5فایلهای تغییر یافته به همراه320 افزوده شده و 40 حذف شده
  1. 7 9
      audio.go
  2. 16 17
      image.go
  3. 15 14
      random.go
  4. 263 0
      siprng.go
  5. 19 0
      siprng_test.go

+ 7 - 9
audio.go

@@ -8,9 +8,7 @@ import (
 	"bytes"
 	"bytes"
 	"encoding/binary"
 	"encoding/binary"
 	"io"
 	"io"
-
 	"math"
 	"math"
-	"math/rand"
 )
 )
 
 
 const sampleRate = 8000 // Hz
 const sampleRate = 8000 // Hz
@@ -51,7 +49,7 @@ func NewAudio(digits []byte, lang string) *Audio {
 	intervals := make([]int, len(digits)+1)
 	intervals := make([]int, len(digits)+1)
 	intdur := 0
 	intdur := 0
 	for i := range intervals {
 	for i := range intervals {
-		dur := rnd(sampleRate, sampleRate*3) // 1 to 3 seconds
+		dur := randInt(sampleRate, sampleRate*3) // 1 to 3 seconds
 		intdur += dur
 		intdur += dur
 		intervals[i] = dur
 		intervals[i] = dur
 	}
 	}
@@ -125,10 +123,10 @@ func (a *Audio) EncodedLen() int {
 func (a *Audio) makeBackgroundSound(length int) []byte {
 func (a *Audio) makeBackgroundSound(length int) []byte {
 	b := makeWhiteNoise(length, 4)
 	b := makeWhiteNoise(length, 4)
 	for i := 0; i < length/(sampleRate/10); i++ {
 	for i := 0; i < length/(sampleRate/10); i++ {
-		snd := reversedSound(a.digitSounds[rand.Intn(10)])
-		snd = changeSpeed(snd, rndf(0.8, 1.4))
-		place := rand.Intn(len(b) - len(snd))
-		setSoundLevel(snd, rndf(0.2, 0.5))
+		snd := reversedSound(a.digitSounds[randIntn(10)])
+		snd = changeSpeed(snd, randFloat(0.8, 1.4))
+		place := randIntn(len(b) - len(snd))
+		setSoundLevel(snd, randFloat(0.2, 0.5))
 		mixSound(b[place:], snd)
 		mixSound(b[place:], snd)
 	}
 	}
 	return b
 	return b
@@ -136,7 +134,7 @@ func (a *Audio) makeBackgroundSound(length int) []byte {
 
 
 func (a *Audio) randomizedDigitSound(n byte) []byte {
 func (a *Audio) randomizedDigitSound(n byte) []byte {
 	s := randomSpeed(a.digitSounds[n])
 	s := randomSpeed(a.digitSounds[n])
-	setSoundLevel(s, rndf(0.75, 1.2))
+	setSoundLevel(s, randFloat(0.75, 1.2))
 	return s
 	return s
 }
 }
 
 
@@ -198,7 +196,7 @@ func changeSpeed(a []byte, speed float64) []byte {
 }
 }
 
 
 func randomSpeed(a []byte) []byte {
 func randomSpeed(a []byte) []byte {
-	pitch := rndf(0.9, 1.2)
+	pitch := randFloat(0.9, 1.2)
 	return changeSpeed(a, pitch)
 	return changeSpeed(a, pitch)
 }
 }
 
 

+ 16 - 17
image.go

@@ -11,7 +11,6 @@ import (
 	"image/png"
 	"image/png"
 	"io"
 	"io"
 	"math"
 	"math"
-	"math/rand"
 )
 )
 
 
 const (
 const (
@@ -37,9 +36,9 @@ func randomPalette() color.Palette {
 	p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
 	p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
 	// Primary color.
 	// Primary color.
 	prim := color.RGBA{
 	prim := color.RGBA{
-		uint8(rand.Intn(129)),
-		uint8(rand.Intn(129)),
-		uint8(rand.Intn(129)),
+		uint8(randIntn(129)),
+		uint8(randIntn(129)),
+		uint8(randIntn(129)),
 		0xFF,
 		0xFF,
 	}
 	}
 	p[1] = prim
 	p[1] = prim
@@ -65,8 +64,8 @@ func NewImage(digits []byte, width, height int) *Image {
 	} else {
 	} else {
 		border = width / 5
 		border = width / 5
 	}
 	}
-	x := rnd(border, maxx-border)
-	y := rnd(border, maxy-border)
+	x := randInt(border, maxx-border)
+	y := randInt(border, maxy-border)
 	// Draw digits.
 	// Draw digits.
 	for _, n := range digits {
 	for _, n := range digits {
 		m.drawDigit(font[n], x, y)
 		m.drawDigit(font[n], x, y)
@@ -75,7 +74,7 @@ func NewImage(digits []byte, width, height int) *Image {
 	// Draw strike-through line.
 	// Draw strike-through line.
 	m.strikeThrough()
 	m.strikeThrough()
 	// Apply wave distortion.
 	// Apply wave distortion.
-	m.distort(rndf(5, 10), rndf(100, 200))
+	m.distort(randFloat(5, 10), randFloat(100, 200))
 	// Fill image with random circles.
 	// Fill image with random circles.
 	m.fillWithCircles(circleCount, m.dotSize)
 	m.fillWithCircles(circleCount, m.dotSize)
 	return m
 	return m
@@ -168,34 +167,34 @@ func (m *Image) fillWithCircles(n, maxradius int) {
 	maxx := m.Bounds().Max.X
 	maxx := m.Bounds().Max.X
 	maxy := m.Bounds().Max.Y
 	maxy := m.Bounds().Max.Y
 	for i := 0; i < n; i++ {
 	for i := 0; i < n; i++ {
-		colorIdx := uint8(rnd(1, circleCount-1))
-		r := rnd(1, maxradius)
-		m.drawCircle(rnd(r, maxx-r), rnd(r, maxy-r), r, colorIdx)
+		colorIdx := uint8(randInt(1, circleCount-1))
+		r := randInt(1, maxradius)
+		m.drawCircle(randInt(r, maxx-r), randInt(r, maxy-r), r, colorIdx)
 	}
 	}
 }
 }
 
 
 func (m *Image) strikeThrough() {
 func (m *Image) strikeThrough() {
 	maxx := m.Bounds().Max.X
 	maxx := m.Bounds().Max.X
 	maxy := m.Bounds().Max.Y
 	maxy := m.Bounds().Max.Y
-	y := rnd(maxy/3, maxy-maxy/3)
-	amplitude := rndf(5, 20)
-	period := rndf(80, 180)
+	y := randInt(maxy/3, maxy-maxy/3)
+	amplitude := randFloat(5, 20)
+	period := randFloat(80, 180)
 	dx := 2.0 * math.Pi / period
 	dx := 2.0 * math.Pi / period
 	for x := 0; x < maxx; x++ {
 	for x := 0; x < maxx; x++ {
 		xo := amplitude * math.Cos(float64(y)*dx)
 		xo := amplitude * math.Cos(float64(y)*dx)
 		yo := amplitude * math.Sin(float64(x)*dx)
 		yo := amplitude * math.Sin(float64(x)*dx)
 		for yn := 0; yn < m.dotSize; yn++ {
 		for yn := 0; yn < m.dotSize; yn++ {
-			r := rnd(0, m.dotSize)
+			r := randInt(0, m.dotSize)
 			m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
 			m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
 		}
 		}
 	}
 	}
 }
 }
 
 
 func (m *Image) drawDigit(digit []byte, x, y int) {
 func (m *Image) drawDigit(digit []byte, x, y int) {
-	skf := rndf(-maxSkew, maxSkew)
+	skf := randFloat(-maxSkew, maxSkew)
 	xs := float64(x)
 	xs := float64(x)
 	r := m.dotSize / 2
 	r := m.dotSize / 2
-	y += rnd(-r, r)
+	y += randInt(-r, r)
 	for yo := 0; yo < fontHeight; yo++ {
 	for yo := 0; yo < fontHeight; yo++ {
 		for xo := 0; xo < fontWidth; xo++ {
 		for xo := 0; xo < fontWidth; xo++ {
 			if digit[yo*fontWidth+xo] != blackChar {
 			if digit[yo*fontWidth+xo] != blackChar {
@@ -232,7 +231,7 @@ func randomBrightness(c color.RGBA, max uint8) color.RGBA {
 	if maxc > max {
 	if maxc > max {
 		return c
 		return c
 	}
 	}
-	n := rand.Intn(int(max-maxc)) - int(minc)
+	n := randIntn(int(max-maxc)) - int(minc)
 	return color.RGBA{
 	return color.RGBA{
 		uint8(int(c.R) + n),
 		uint8(int(c.R) + n),
 		uint8(int(c.G) + n),
 		uint8(int(c.G) + n),

+ 15 - 14
random.go

@@ -5,10 +5,8 @@
 package captcha
 package captcha
 
 
 import (
 import (
-	crand "crypto/rand"
+	"crypto/rand"
 	"io"
 	"io"
-	"math/rand"
-	"time"
 )
 )
 
 
 // idLen is a length of captcha id string.
 // idLen is a length of captcha id string.
@@ -17,10 +15,6 @@ const idLen = 20
 // idChars are characters allowed in captcha id.
 // idChars are characters allowed in captcha id.
 var idChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
 var idChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
 
 
-func init() {
-	rand.Seed(time.Now().UnixNano())
-}
-
 // RandomDigits returns a byte slice of the given length containing
 // RandomDigits returns a byte slice of the given length containing
 // pseudorandom numbers in range 0-9. The slice can be used as a captcha
 // pseudorandom numbers in range 0-9. The slice can be used as a captcha
 // solution.
 // solution.
@@ -31,7 +25,7 @@ func RandomDigits(length int) []byte {
 // randomBytes returns a byte slice of the given length read from CSPRNG.
 // randomBytes returns a byte slice of the given length read from CSPRNG.
 func randomBytes(length int) (b []byte) {
 func randomBytes(length int) (b []byte) {
 	b = make([]byte, length)
 	b = make([]byte, length)
-	if _, err := io.ReadFull(crand.Reader, b); err != nil {
+	if _, err := io.ReadFull(rand.Reader, b); err != nil {
 		panic("captcha: error reading random source: " + err.Error())
 		panic("captcha: error reading random source: " + err.Error())
 	}
 	}
 	return
 	return
@@ -69,12 +63,19 @@ func randomId() string {
 	return string(b)
 	return string(b)
 }
 }
 
 
-// rnd returns a non-crypto pseudorandom int in range [from, to].
-func rnd(from, to int) int {
-	return rand.Intn(to+1-from) + from
+var prng = &siprng{}
+
+// randIntn returns a pseudorandom non-negative int in range [0, n).
+func randIntn(n int) int {
+	return prng.Intn(n)
+}
+
+// randInt returns a pseudorandom int in range [from, to].
+func randInt(from, to int) int {
+	return prng.Intn(to+1-from) + from
 }
 }
 
 
-// rndf returns a non-crypto pseudorandom float64 in range [from, to].
-func rndf(from, to float64) float64 {
-	return (to-from)*rand.Float64() + from
+// randFloat returns a pseudorandom float64 in range [from, to].
+func randFloat(from, to float64) float64 {
+	return (to-from)*prng.Float64() + from
 }
 }

+ 263 - 0
siprng.go

@@ -0,0 +1,263 @@
+package captcha
+
+import (
+	"crypto/rand"
+	"encoding/binary"
+	"io"
+	"sync"
+)
+
+// siprng is PRNG based on SipHash-2-4.
+type siprng struct {
+	mu          sync.Mutex
+	k0, k1, ctr uint64
+}
+
+// siphash implements SipHash-2-4, accepting a uint64 as a message.
+func siphash(k0, k1, m uint64) uint64 {
+	// Initialization.
+	v0 := k0 ^ 0x736f6d6570736575
+	v1 := k1 ^ 0x646f72616e646f6d
+	v2 := k0 ^ 0x6c7967656e657261
+	v3 := k1 ^ 0x7465646279746573
+	t := uint64(8) << 56
+
+	// Compression.
+	v3 ^= m
+
+	// Round 1.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	// Round 2.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	v0 ^= m
+
+	// Compress last block.
+	v3 ^= t
+
+	// Round 1.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	// Round 2.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	v0 ^= t
+
+	// Finalization.
+	v2 ^= 0xff
+
+	// Round 1.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	// Round 2.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	// Round 3.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	// Round 4.
+	v0 += v1
+	v1 = v1<<13 | v1>>(64-13)
+	v1 ^= v0
+	v0 = v0<<32 | v0>>(64-32)
+
+	v2 += v3
+	v3 = v3<<16 | v3>>(64-16)
+	v3 ^= v2
+
+	v0 += v3
+	v3 = v3<<21 | v3>>(64-21)
+	v3 ^= v0
+
+	v2 += v1
+	v1 = v1<<17 | v1>>(64-17)
+	v1 ^= v2
+	v2 = v2<<32 | v2>>(64-32)
+
+	return v0 ^ v1 ^ v2 ^ v3
+}
+
+// rekey sets a new PRNG key, which is read from crypto/rand.
+func (p *siprng) rekey() {
+	var k [16]byte
+	if _, err := io.ReadFull(rand.Reader, k[:]); err != nil {
+		panic(err.Error())
+	}
+	p.k0 = binary.LittleEndian.Uint64(k[0:8])
+	p.k1 = binary.LittleEndian.Uint64(k[8:16])
+	p.ctr = 1
+}
+
+// Uint64 returns a new pseudorandom uint64.
+// It rekeys PRNG on the first call and every 64 MB of generated data.
+func (p *siprng) Uint64() uint64 {
+	p.mu.Lock()
+	if p.ctr == 0 || p.ctr > 8*1024*1024 {
+		p.rekey()
+	}
+	v := siphash(p.k0, p.k1, p.ctr)
+	p.ctr++
+	p.mu.Unlock()
+	return v
+}
+
+func (p *siprng) Int63() int64 {
+	return int64(p.Uint64() & 0x7fffffffffffffff)
+}
+
+func (p *siprng) Uint32() uint32 {
+	return uint32(p.Uint64())
+}
+
+func (p *siprng) Int31() int32 {
+	return int32(p.Uint32() & 0x7fffffff)
+}
+
+func (p *siprng) Intn(n int) int {
+	if n <= 0 {
+		panic("invalid argument to Intn")
+	}
+	if n <= 1<<31-1 {
+		return int(p.Int31n(int32(n)))
+	}
+	return int(p.Int63n(int64(n)))
+}
+
+func (p *siprng) Int63n(n int64) int64 {
+	if n <= 0 {
+		panic("invalid argument to Int63n")
+	}
+	max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
+	v := p.Int63()
+	for v > max {
+		v = p.Int63()
+	}
+	return v % n
+}
+
+func (p *siprng) Int31n(n int32) int32 {
+	if n <= 0 {
+		panic("invalid argument to Int31n")
+	}
+	max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
+	v := p.Int31()
+	for v > max {
+		v = p.Int31()
+	}
+	return v % n
+}
+
+func (p *siprng) Float64() float64 { return float64(p.Int63()) / (1 << 63) }

+ 19 - 0
siprng_test.go

@@ -0,0 +1,19 @@
+package captcha
+
+import "testing"
+
+func TestSiphash(t *testing.T) {
+	good := uint64(0xe849e8bb6ffe2567)
+	cur := siphash(0, 0, 0)
+	if cur != good {
+		t.Fatalf("siphash: expected %x, got %x", good, cur)
+	}
+}
+
+func BenchmarkSiprng(b *testing.B) {
+	b.SetBytes(8)
+	p := &siprng{};
+	for i := 0; i < b.N; i++ {
+		p.Uint64()
+	}
+}