Procházet zdrojové kódy

Generate captcha representations deterministically.

WARNING: introduces API incompatibility!

This package generates captcha representations on-the-fly; for instance,
if captcha solution was "123456", every call to NewImage() using this
sequence of digits would generate a different random image containing
"123456"; similarly, NewAudio() would generate a different audio
pronouncing the same sequence: 1, 2, 3, 4, 5, 6.

If a user, instead of storing generated outputs, exposes this
functionality from their server, which is the default and recommended
behaviour, an attacker could try loading the same image or audio over
and over again in attempt to arrive at the most correct optical/voice
recognition result.

Instead of using a global non-deterministic pseudorandom number
generator to distort images and audio, this commit introduces a
deterministic PRNG for each image/audio. This PRNG uses a combination of
a global secret key (generated once during initialization from a system
CSPRNG) and captcha id and solution to produce pseudorandom numbers for
each representation deterministically. Thus, calling NewImage() with the
same captcha id and solution at different times will result in the same
image (ditto for NewAudio).

To make results unique not only for different solutions, but also for
ids, these incompatible changes to public API have been introduced:

NewImage and NewAudio changed from:

  func NewImage(digits []byte, width, height int) *Image
  func NewAudio(digits []byte, lang string) *Audio

to:

  func NewImage(id string, digits []byte, width, height int) *Image
  func NewAudio(id string, digits []byte, lang string) *Audio

That is, they now accept an additional captcha `id` argument.
No other interfaces changed.

Described changes also improved performance of generating captchas.
Dmitry Chestnykh před 11 roky
rodič
revize
90158fbef4
13 změnil soubory, kde provedl 211 přidání a 151 odebrání
  1. 0 23
      .gitignore
  2. 1 1
      LICENSE
  3. 8 8
      README.md
  4. 32 29
      audio.go
  5. 4 2
      audio_test.go
  6. 2 2
      capgen/main.go
  7. 8 8
      captcha.go
  8. 41 36
      image.go
  9. 4 2
      image_test.go
  10. 39 18
      random.go
  11. 1 1
      server.go
  12. 34 19
      siprng.go
  13. 37 2
      siprng_test.go

+ 0 - 23
.gitignore

@@ -1,26 +1,3 @@
-# Compiled Object files, Static and Dynamic libs (Shared Objects)
-*.o
-*.a
-*.so
-
-# Folders
-_obj
-_test
-
-# Architecture specific extensions/prefixes
-*.[568vq]
-[568vq].out
-
-*.cgo1.go
-*.cgo2.c
-_cgo_defun.c
-_cgo_gotypes.go
-_cgo_export.*
-
-_testmain.go
-
-*.exe
- 
 # Generated test captchas
 # Generated test captchas
 capgen/*.png
 capgen/*.png
 capgen/*.wav
 capgen/*.wav

+ 1 - 1
LICENSE

@@ -1,4 +1,4 @@
-Copyright (c) 2011 Dmitry Chestnykh
+Copyright (c) 2011-2014 Dmitry Chestnykh <dmitry@codingrobots.com>
 
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 of this software and associated documentation files (the "Software"), to deal

+ 8 - 8
README.md

@@ -32,13 +32,13 @@ registering the object with SetCustomStore.
 
 
 Captchas are created by calling New, which returns the captcha id.  Their
 Captchas are created by calling New, which returns the captcha id.  Their
 representations, though, are created on-the-fly by calling WriteImage or
 representations, though, are created on-the-fly by calling WriteImage or
-WriteAudio functions. Created representations are not stored anywhere, so
+WriteAudio functions. Created representations are not stored anywhere, but
 subsequent calls to these functions with the same id will write the same
 subsequent calls to these functions with the same id will write the same
-captcha solution, but with a different random representation. Reload
-function will create a new different solution for the provided captcha,
-allowing users to "reload" captcha if they can't solve the displayed one
-without reloading the whole page.  Verify and VerifyString are used to
-verify that the given solution is the right one for the given captcha id.
+captcha solution. Reload function will create a new different solution for the
+provided captcha, allowing users to "reload" captcha if they can't solve the
+displayed one without reloading the whole page.  Verify and VerifyString are
+used to verify that the given solution is the right one for the given captcha
+id.
 
 
 Server provides an http.Handler which can serve image and audio
 Server provides an http.Handler which can serve image and audio
 representations of captchas automatically from the URL. It can also be used
 representations of captchas automatically from the URL. It can also be used
@@ -205,7 +205,7 @@ type Audio struct {
 
 
 ### func NewAudio
 ### func NewAudio
 
 
-	func NewAudio(digits []byte, lang string) *Audio
+	func NewAudio(id string, digits []byte, lang string) *Audio
 	
 	
 NewAudio returns a new audio captcha with the given digits, where each digit
 NewAudio returns a new audio captcha with the given digits, where each digit
 must be in range 0-9. Digits are pronounced in the given language. If there
 must be in range 0-9. Digits are pronounced in the given language. If there
@@ -236,7 +236,7 @@ type Image struct {
 
 
 ### func NewImage
 ### func NewImage
 
 
-	func NewImage(digits []byte, width, height int) *Image
+	func NewImage(id string, digits []byte, width, height int) *Image
 	
 	
 NewImage returns a new captcha image of the given width and height with the
 NewImage returns a new captcha image of the given width and height with the
 given digits, where each digit must be in range 0-9.
 given digits, where each digit must be in range 0-9.

+ 32 - 29
audio.go

@@ -1,4 +1,4 @@
-// Copyright 2011 Dmitry Chestnykh. All rights reserved.
+// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 // license that can be found in the LICENSE file.
 
 
@@ -13,9 +13,7 @@ import (
 
 
 const sampleRate = 8000 // Hz
 const sampleRate = 8000 // Hz
 
 
-var (
-	endingBeepSound []byte
-)
+var endingBeepSound []byte
 
 
 func init() {
 func init() {
 	endingBeepSound = changeSpeed(beepSound, 1.4)
 	endingBeepSound = changeSpeed(beepSound, 1.4)
@@ -24,6 +22,7 @@ func init() {
 type Audio struct {
 type Audio struct {
 	body        *bytes.Buffer
 	body        *bytes.Buffer
 	digitSounds [][]byte
 	digitSounds [][]byte
+	rng         siprng
 }
 }
 
 
 // NewAudio returns a new audio captcha with the given digits, where each digit
 // NewAudio returns a new audio captcha with the given digits, where each digit
@@ -31,8 +30,12 @@ type Audio struct {
 // are no sounds for the given language, English is used.
 // are no sounds for the given language, English is used.
 //
 //
 // Possible values for lang are "en", "ru", "zh".
 // Possible values for lang are "en", "ru", "zh".
-func NewAudio(digits []byte, lang string) *Audio {
+func NewAudio(id string, digits []byte, lang string) *Audio {
 	a := new(Audio)
 	a := new(Audio)
+
+	// Initialize PRNG.
+	a.rng.Seed(deriveSeed(audioSeedPurpose, id, digits))
+
 	if sounds, ok := digitSounds[lang]; ok {
 	if sounds, ok := digitSounds[lang]; ok {
 		a.digitSounds = sounds
 		a.digitSounds = sounds
 	} else {
 	} else {
@@ -49,7 +52,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 := randInt(sampleRate, sampleRate*3) // 1 to 3 seconds
+		dur := a.rng.Int(sampleRate, sampleRate*3) // 1 to 3 seconds
 		intdur += dur
 		intdur += dur
 		intervals[i] = dur
 		intervals[i] = dur
 	}
 	}
@@ -121,20 +124,20 @@ func (a *Audio) EncodedLen() int {
 }
 }
 
 
 func (a *Audio) makeBackgroundSound(length int) []byte {
 func (a *Audio) makeBackgroundSound(length int) []byte {
-	b := makeWhiteNoise(length, 4)
+	b := a.makeWhiteNoise(length, 4)
 	for i := 0; i < length/(sampleRate/10); i++ {
 	for i := 0; i < length/(sampleRate/10); i++ {
-		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))
+		snd := reversedSound(a.digitSounds[a.rng.Intn(10)])
+		snd = changeSpeed(snd, a.rng.Float(0.8, 1.4))
+		place := a.rng.Intn(len(b) - len(snd))
+		setSoundLevel(snd, a.rng.Float(0.2, 0.5))
 		mixSound(b[place:], snd)
 		mixSound(b[place:], snd)
 	}
 	}
 	return b
 	return b
 }
 }
 
 
 func (a *Audio) randomizedDigitSound(n byte) []byte {
 func (a *Audio) randomizedDigitSound(n byte) []byte {
-	s := randomSpeed(a.digitSounds[n])
-	setSoundLevel(s, randFloat(0.75, 1.2))
+	s := a.randomSpeed(a.digitSounds[n])
+	setSoundLevel(s, a.rng.Float(0.75, 1.2))
 	return s
 	return s
 }
 }
 
 
@@ -148,6 +151,22 @@ func (a *Audio) longestDigitSndLen() int {
 	return n
 	return n
 }
 }
 
 
+func (a *Audio) randomSpeed(b []byte) []byte {
+	pitch := a.rng.Float(0.9, 1.2)
+	return changeSpeed(b, pitch)
+}
+
+func (a *Audio) makeWhiteNoise(length int, level uint8) []byte {
+	noise := a.rng.Bytes(length)
+	adj := 128 - level/2
+	for i, v := range noise {
+		v %= level
+		v += adj
+		noise[i] = v
+	}
+	return noise
+}
+
 // mixSound mixes src into dst. Dst must have length equal to or greater than
 // mixSound mixes src into dst. Dst must have length equal to or greater than
 // src length.
 // src length.
 func mixSound(dst, src []byte) {
 func mixSound(dst, src []byte) {
@@ -195,11 +214,6 @@ func changeSpeed(a []byte, speed float64) []byte {
 	return b
 	return b
 }
 }
 
 
-func randomSpeed(a []byte) []byte {
-	pitch := randFloat(0.9, 1.2)
-	return changeSpeed(a, pitch)
-}
-
 func makeSilence(length int) []byte {
 func makeSilence(length int) []byte {
 	b := make([]byte, length)
 	b := make([]byte, length)
 	for i := range b {
 	for i := range b {
@@ -208,17 +222,6 @@ func makeSilence(length int) []byte {
 	return b
 	return b
 }
 }
 
 
-func makeWhiteNoise(length int, level uint8) []byte {
-	noise := randomBytes(length)
-	adj := 128 - level/2
-	for i, v := range noise {
-		v %= level
-		v += adj
-		noise[i] = v
-	}
-	return noise
-}
-
 func reversedSound(a []byte) []byte {
 func reversedSound(a []byte) []byte {
 	n := len(a)
 	n := len(a)
 	b := make([]byte, n)
 	b := make([]byte, n)

+ 4 - 2
audio_test.go

@@ -12,18 +12,20 @@ import (
 func BenchmarkNewAudio(b *testing.B) {
 func BenchmarkNewAudio(b *testing.B) {
 	b.StopTimer()
 	b.StopTimer()
 	d := RandomDigits(DefaultLen)
 	d := RandomDigits(DefaultLen)
+	id := randomId()
 	b.StartTimer()
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		NewAudio(d, "")
+		NewAudio(id, d, "")
 	}
 	}
 }
 }
 
 
 func BenchmarkAudioWriteTo(b *testing.B) {
 func BenchmarkAudioWriteTo(b *testing.B) {
 	b.StopTimer()
 	b.StopTimer()
 	d := RandomDigits(DefaultLen)
 	d := RandomDigits(DefaultLen)
+	id := randomId()
 	b.StartTimer()
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		a := NewAudio(d, "")
+		a := NewAudio(id, d, "")
 		n, _ := a.WriteTo(ioutil.Discard)
 		n, _ := a.WriteTo(ioutil.Discard)
 		b.SetBytes(n)
 		b.SetBytes(n)
 	}
 	}

+ 2 - 2
capgen/main.go

@@ -44,9 +44,9 @@ func main() {
 	d := captcha.RandomDigits(*flagLen)
 	d := captcha.RandomDigits(*flagLen)
 	switch {
 	switch {
 	case *flagAudio:
 	case *flagAudio:
-		w = captcha.NewAudio(d, *flagLang)
+		w = captcha.NewAudio("", d, *flagLang)
 	case *flagImage:
 	case *flagImage:
-		w = captcha.NewImage(d, *flagImgW, *flagImgH)
+		w = captcha.NewImage("", d, *flagImgW, *flagImgH)
 	}
 	}
 	_, err = w.WriteTo(f)
 	_, err = w.WriteTo(f)
 	if err != nil {
 	if err != nil {

+ 8 - 8
captcha.go

@@ -31,13 +31,13 @@
 //
 //
 // Captchas are created by calling New, which returns the captcha id.  Their
 // Captchas are created by calling New, which returns the captcha id.  Their
 // representations, though, are created on-the-fly by calling WriteImage or
 // representations, though, are created on-the-fly by calling WriteImage or
-// WriteAudio functions. Created representations are not stored anywhere, so
+// WriteAudio functions. Created representations are not stored anywhere, but
 // subsequent calls to these functions with the same id will write the same
 // subsequent calls to these functions with the same id will write the same
-// captcha solution, but with a different random representation. Reload
-// function will create a new different solution for the provided captcha,
-// allowing users to "reload" captcha if they can't solve the displayed one
-// without reloading the whole page.  Verify and VerifyString are used to
-// verify that the given solution is the right one for the given captcha id.
+// captcha solution. Reload function will create a new different solution for
+// the provided captcha, allowing users to "reload" captcha if they can't solve
+// the displayed one without reloading the whole page.  Verify and VerifyString
+// are used to verify that the given solution is the right one for the given
+// captcha id.
 //
 //
 // Server provides an http.Handler which can serve image and audio
 // Server provides an http.Handler which can serve image and audio
 // representations of captchas automatically from the URL. It can also be used
 // representations of captchas automatically from the URL. It can also be used
@@ -110,7 +110,7 @@ func WriteImage(w io.Writer, id string, width, height int) error {
 	if d == nil {
 	if d == nil {
 		return ErrNotFound
 		return ErrNotFound
 	}
 	}
-	_, err := NewImage(d, width, height).WriteTo(w)
+	_, err := NewImage(id, d, width, height).WriteTo(w)
 	return err
 	return err
 }
 }
 
 
@@ -122,7 +122,7 @@ func WriteAudio(w io.Writer, id string, lang string) error {
 	if d == nil {
 	if d == nil {
 		return ErrNotFound
 		return ErrNotFound
 	}
 	}
-	_, err := NewAudio(d, lang).WriteTo(w)
+	_, err := NewAudio(id, d, lang).WriteTo(w)
 	return err
 	return err
 }
 }
 
 

+ 41 - 36
image.go

@@ -1,4 +1,4 @@
-// Copyright 2011 Dmitry Chestnykh. All rights reserved.
+// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 // license that can be found in the LICENSE file.
 
 
@@ -28,32 +28,18 @@ type Image struct {
 	numWidth  int
 	numWidth  int
 	numHeight int
 	numHeight int
 	dotSize   int
 	dotSize   int
-}
-
-func randomPalette() color.Palette {
-	p := make([]color.Color, circleCount+1)
-	// Transparent color.
-	p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
-	// Primary color.
-	prim := color.RGBA{
-		uint8(randIntn(129)),
-		uint8(randIntn(129)),
-		uint8(randIntn(129)),
-		0xFF,
-	}
-	p[1] = prim
-	// Circle colors.
-	for i := 2; i <= circleCount; i++ {
-		p[i] = randomBrightness(prim, 255)
-	}
-	return p
+	rng       siprng
 }
 }
 
 
 // NewImage returns a new captcha image of the given width and height with the
 // NewImage returns a new captcha image of the given width and height with the
 // given digits, where each digit must be in range 0-9.
 // given digits, where each digit must be in range 0-9.
-func NewImage(digits []byte, width, height int) *Image {
+func NewImage(id string, digits []byte, width, height int) *Image {
 	m := new(Image)
 	m := new(Image)
-	m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), randomPalette())
+
+	// Initialize PRNG.
+	m.rng.Seed(deriveSeed(imageSeedPurpose, id, digits))
+
+	m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), m.getRandomPalette())
 	m.calculateSizes(width, height, len(digits))
 	m.calculateSizes(width, height, len(digits))
 	// Randomly position captcha inside the image.
 	// Randomly position captcha inside the image.
 	maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
 	maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
@@ -64,8 +50,8 @@ func NewImage(digits []byte, width, height int) *Image {
 	} else {
 	} else {
 		border = width / 5
 		border = width / 5
 	}
 	}
-	x := randInt(border, maxx-border)
-	y := randInt(border, maxy-border)
+	x := m.rng.Int(border, maxx-border)
+	y := m.rng.Int(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)
@@ -74,12 +60,31 @@ 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(randFloat(5, 10), randFloat(100, 200))
+	m.distort(m.rng.Float(5, 10), m.rng.Float(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
 }
 }
 
 
+func (m *Image) getRandomPalette() color.Palette {
+	p := make([]color.Color, circleCount+1)
+	// Transparent color.
+	p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
+	// Primary color.
+	prim := color.RGBA{
+		uint8(m.rng.Intn(129)),
+		uint8(m.rng.Intn(129)),
+		uint8(m.rng.Intn(129)),
+		0xFF,
+	}
+	p[1] = prim
+	// Circle colors.
+	for i := 2; i <= circleCount; i++ {
+		p[i] = m.randomBrightness(prim, 255)
+	}
+	return p
+}
+
 // encodedPNG encodes an image to PNG and returns
 // encodedPNG encodes an image to PNG and returns
 // the result as a byte slice.
 // the result as a byte slice.
 func (m *Image) encodedPNG() []byte {
 func (m *Image) encodedPNG() []byte {
@@ -167,34 +172,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(randInt(1, circleCount-1))
-		r := randInt(1, maxradius)
-		m.drawCircle(randInt(r, maxx-r), randInt(r, maxy-r), r, colorIdx)
+		colorIdx := uint8(m.rng.Int(1, circleCount-1))
+		r := m.rng.Int(1, maxradius)
+		m.drawCircle(m.rng.Int(r, maxx-r), m.rng.Int(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 := randInt(maxy/3, maxy-maxy/3)
-	amplitude := randFloat(5, 20)
-	period := randFloat(80, 180)
+	y := m.rng.Int(maxy/3, maxy-maxy/3)
+	amplitude := m.rng.Float(5, 20)
+	period := m.rng.Float(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 := randInt(0, m.dotSize)
+			r := m.rng.Int(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 := randFloat(-maxSkew, maxSkew)
+	skf := m.rng.Float(-maxSkew, maxSkew)
 	xs := float64(x)
 	xs := float64(x)
 	r := m.dotSize / 2
 	r := m.dotSize / 2
-	y += randInt(-r, r)
+	y += m.rng.Int(-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 {
@@ -225,13 +230,13 @@ func (m *Image) distort(amplude float64, period float64) {
 	m.Paletted = newm
 	m.Paletted = newm
 }
 }
 
 
-func randomBrightness(c color.RGBA, max uint8) color.RGBA {
+func (m *Image) randomBrightness(c color.RGBA, max uint8) color.RGBA {
 	minc := min3(c.R, c.G, c.B)
 	minc := min3(c.R, c.G, c.B)
 	maxc := max3(c.R, c.G, c.B)
 	maxc := max3(c.R, c.G, c.B)
 	if maxc > max {
 	if maxc > max {
 		return c
 		return c
 	}
 	}
-	n := randIntn(int(max-maxc)) - int(minc)
+	n := m.rng.Intn(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),

+ 4 - 2
image_test.go

@@ -18,19 +18,21 @@ func (bc *byteCounter) Write(b []byte) (int, error) {
 func BenchmarkNewImage(b *testing.B) {
 func BenchmarkNewImage(b *testing.B) {
 	b.StopTimer()
 	b.StopTimer()
 	d := RandomDigits(DefaultLen)
 	d := RandomDigits(DefaultLen)
+	id := randomId()
 	b.StartTimer()
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		NewImage(d, StdWidth, StdHeight)
+		NewImage(id, d, StdWidth, StdHeight)
 	}
 	}
 }
 }
 
 
 func BenchmarkImageWriteTo(b *testing.B) {
 func BenchmarkImageWriteTo(b *testing.B) {
 	b.StopTimer()
 	b.StopTimer()
 	d := RandomDigits(DefaultLen)
 	d := RandomDigits(DefaultLen)
+	id := randomId()
 	b.StartTimer()
 	b.StartTimer()
 	counter := &byteCounter{}
 	counter := &byteCounter{}
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		img := NewImage(d, StdWidth, StdHeight)
+		img := NewImage(id, d, StdWidth, StdHeight)
 		img.WriteTo(counter)
 		img.WriteTo(counter)
 		b.SetBytes(counter.n)
 		b.SetBytes(counter.n)
 		counter.n = 0
 		counter.n = 0

+ 39 - 18
random.go

@@ -1,20 +1,58 @@
-// Copyright 2011 Dmitry Chestnykh. All rights reserved.
+// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 // license that can be found in the LICENSE file.
 
 
 package captcha
 package captcha
 
 
 import (
 import (
+	"crypto/hmac"
 	"crypto/rand"
 	"crypto/rand"
+	"crypto/sha256"
 	"io"
 	"io"
 )
 )
 
 
 // idLen is a length of captcha id string.
 // idLen is a length of captcha id string.
+// (20 bytes of 62-letter alphabet give ~119 bits.)
 const idLen = 20
 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")
 
 
+// rngKey is a secret key used to deterministically derive seeds for
+// PRNGs used in image and audio. Generated once during initialization.
+var rngKey [32]byte
+
+func init() {
+	if _, err := io.ReadFull(rand.Reader, rngKey[:]); err != nil {
+		panic("captcha: error reading random source: " + err.Error())
+	}
+}
+
+// Purposes for seed derivation. The goal is to make deterministic PRNG produce
+// different outputs for images and audio by using different derived seeds.
+const (
+	imageSeedPurpose = 0x01
+	audioSeedPurpose = 0x02
+)
+
+// deriveSeed returns a 16-byte PRNG seed from rngKey, purpose, id and digits.
+// Same purpose, id and digits will result in the same derived seed for this
+// instance of running application.
+//
+//   out = HMAC(rngKey, purpose || id || 0x00 || digits)  (cut to 16 bytes)
+//
+func deriveSeed(purpose byte, id string, digits []byte) (out [16]byte) {
+	var buf [sha256.Size]byte
+	h := hmac.New(sha256.New, rngKey[:])
+	h.Write([]byte{purpose})
+	io.WriteString(h, id)
+	h.Write([]byte{0})
+	h.Write(digits)
+	sum := h.Sum(buf[:0])
+	copy(out[:], sum)
+	return
+}
+
 // 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.
@@ -62,20 +100,3 @@ func randomId() string {
 	}
 	}
 	return string(b)
 	return string(b)
 }
 }
-
-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
-}
-
-// randFloat returns a pseudorandom float64 in range [from, to].
-func randFloat(from, to float64) float64 {
-	return (to-from)*prng.Float64() + from
-}

+ 1 - 1
server.go

@@ -60,7 +60,7 @@ func (h *captchaHandler) serve(w http.ResponseWriter, id, ext string, lang strin
 		if d == nil {
 		if d == nil {
 			return ErrNotFound
 			return ErrNotFound
 		}
 		}
-		a := NewAudio(d, lang)
+		a := NewAudio(id, d, lang)
 		if !download {
 		if !download {
 			w.Header().Set("Content-Type", "audio/x-wav")
 			w.Header().Set("Content-Type", "audio/x-wav")
 		}
 		}

+ 34 - 19
siprng.go

@@ -1,15 +1,14 @@
+// Copyright 2014 Dmitry Chestnykh. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
 package captcha
 package captcha
 
 
-import (
-	"crypto/rand"
-	"encoding/binary"
-	"io"
-	"sync"
-)
+import "encoding/binary"
 
 
 // siprng is PRNG based on SipHash-2-4.
 // siprng is PRNG based on SipHash-2-4.
+// (Note: it's not safe to use a single siprng from multiple goroutines.)
 type siprng struct {
 type siprng struct {
-	mu          sync.Mutex
 	k0, k1, ctr uint64
 	k0, k1, ctr uint64
 }
 }
 
 
@@ -190,30 +189,36 @@ func siphash(k0, k1, m uint64) uint64 {
 	return v0 ^ v1 ^ v2 ^ v3
 	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())
-	}
+// SetSeed sets a new secret seed for PRNG.
+func (p *siprng) Seed(k [16]byte) {
 	p.k0 = binary.LittleEndian.Uint64(k[0:8])
 	p.k0 = binary.LittleEndian.Uint64(k[0:8])
 	p.k1 = binary.LittleEndian.Uint64(k[8:16])
 	p.k1 = binary.LittleEndian.Uint64(k[8:16])
 	p.ctr = 1
 	p.ctr = 1
 }
 }
 
 
 // Uint64 returns a new pseudorandom uint64.
 // 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 {
 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)
 	v := siphash(p.k0, p.k1, p.ctr)
 	p.ctr++
 	p.ctr++
-	p.mu.Unlock()
 	return v
 	return v
 }
 }
 
 
+func (p *siprng) Bytes(n int) []byte {
+	// Since we don't have a buffer for generated bytes in siprng state,
+	// we just generate enough 8-byte blocks and then cut the result to the
+	// required length. Doing it this way, we lose generated bytes, and we
+	// don't get the strictly sequential deterministic output from PRNG:
+	// calling Uint64() and then Bytes(3) produces different output than
+	// when calling them in the reverse order, but for our applications
+	// this is OK.
+	numBlocks := (n + 8 - 1) / 8
+	b := make([]byte, numBlocks*8)
+	for i := 0; i < len(b); i += 8 {
+		binary.LittleEndian.PutUint64(b[i:], p.Uint64())
+	}
+	return b[:n]
+}
+
 func (p *siprng) Int63() int64 {
 func (p *siprng) Int63() int64 {
 	return int64(p.Uint64() & 0x7fffffffffffffff)
 	return int64(p.Uint64() & 0x7fffffffffffffff)
 }
 }
@@ -261,3 +266,13 @@ func (p *siprng) Int31n(n int32) int32 {
 }
 }
 
 
 func (p *siprng) Float64() float64 { return float64(p.Int63()) / (1 << 63) }
 func (p *siprng) Float64() float64 { return float64(p.Int63()) / (1 << 63) }
+
+// Int returns a pseudorandom int in range [from, to].
+func (p *siprng) Int(from, to int) int {
+	return p.Intn(to+1-from) + from
+}
+
+// Float returns a pseudorandom float64 in range [from, to].
+func (p *siprng) Float(from, to float64) float64 {
+	return (to-from)*p.Float64() + from
+}

+ 37 - 2
siprng_test.go

@@ -1,6 +1,9 @@
 package captcha
 package captcha
 
 
-import "testing"
+import (
+	"bytes"
+	"testing"
+)
 
 
 func TestSiphash(t *testing.T) {
 func TestSiphash(t *testing.T) {
 	good := uint64(0xe849e8bb6ffe2567)
 	good := uint64(0xe849e8bb6ffe2567)
@@ -10,9 +13,41 @@ func TestSiphash(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestSiprng(t *testing.T) {
+	m := make(map[uint64]interface{})
+	var yes interface{}
+	r := siprng{}
+	r.Seed([16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})
+	for i := 0; i < 100000; i++ {
+		v := r.Uint64()
+		if _, ok := m[v]; ok {
+			t.Errorf("siphash: collision on %d: %x", i, v)
+		}
+		m[v] = yes
+	}
+}
+
+func TestSiprngBytes(t *testing.T) {
+	r := siprng{}
+	r.Seed([16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})
+	x := r.Bytes(32)
+	if len(x) != 32 {
+		t.Fatalf("siphash: wrong length: expected 32, got %d", len(x))
+	}
+	y := r.Bytes(32)
+	if bytes.Equal(x, y) {
+		t.Fatalf("siphash: stream repeats: %x = %x", x, y)
+	}
+	r.Seed([16]byte{})
+	z := r.Bytes(32)
+	if bytes.Equal(z, x) {
+		t.Fatalf("siphash: outputs under different keys repeat: %x = %x", z, x)
+	}
+}
+
 func BenchmarkSiprng(b *testing.B) {
 func BenchmarkSiprng(b *testing.B) {
 	b.SetBytes(8)
 	b.SetBytes(8)
-	p := &siprng{};
+	p := &siprng{}
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
 		p.Uint64()
 		p.Uint64()
 	}
 	}