Remove config and use service workers for goroutines

This commit is contained in:
Daniel 2020-04-16 13:13:40 +02:00
parent c58d6a0f30
commit 30a6948009
7 changed files with 125 additions and 179 deletions

View file

@ -1,17 +1,20 @@
package rng package rng
import ( import (
"context"
"encoding/binary" "encoding/binary"
"github.com/tevino/abool" "github.com/tevino/abool"
"github.com/safing/portbase/config"
"github.com/safing/portbase/container" "github.com/safing/portbase/container"
) )
const (
minFeedEntropy = 256
)
var ( var (
rngFeeder = make(chan []byte) rngFeeder = make(chan []byte)
minFeedEntropy config.IntOption
) )
// The Feeder is used to feed entropy to the RNG. // The Feeder is used to feed entropy to the RNG.
@ -34,7 +37,7 @@ func NewFeeder() *Feeder {
needsEntropy: abool.NewBool(true), needsEntropy: abool.NewBool(true),
buffer: container.New(), buffer: container.New(),
} }
go new.run() module.StartServiceWorker("feeder", 0, new.run)
return new return new
} }
@ -87,7 +90,7 @@ func (f *Feeder) CloseFeeder() {
f.input <- nil f.input <- nil
} }
func (f *Feeder) run() { func (f *Feeder) run(ctx context.Context) error {
defer f.needsEntropy.UnSet() defer f.needsEntropy.UnSet()
for { for {
@ -97,23 +100,26 @@ func (f *Feeder) run() {
for { for {
select { select {
case newEntropy := <-f.input: case newEntropy := <-f.input:
if newEntropy != nil { // check if feed has been closed
f.buffer.Append(newEntropy.data) if newEntropy == nil {
f.entropy += int64(newEntropy.entropy) return nil
if f.entropy >= minFeedEntropy() {
break gather
}
} }
case <-shutdownSignal: // append to buffer
return f.buffer.Append(newEntropy.data)
f.entropy += int64(newEntropy.entropy)
if f.entropy >= minFeedEntropy {
break gather
}
case <-ctx.Done():
return nil
} }
} }
// feed // feed
f.needsEntropy.UnSet() f.needsEntropy.UnSet()
select { select {
case rngFeeder <- f.buffer.CompileData(): case rngFeeder <- f.buffer.CompileData():
case <-shutdownSignal: case <-ctx.Done():
return return nil
} }
f.buffer = container.New() f.buffer = container.New()
} }

View file

@ -1,29 +1,27 @@
package rng package rng
import ( import (
"context"
"time" "time"
) )
var (
fullFeedDuration = 100 * time.Millisecond
)
func getFullFeedDuration() time.Duration { func getFullFeedDuration() time.Duration {
// full feed every 5x time of reseedAfterSeconds // full feed every 5x time of reseedAfterSeconds
secsUntilFullFeed := reseedAfterSeconds() * 5 secsUntilFullFeed := reseedAfterSeconds * 5
// full feed at most once per minute // full feed at most once every ten minutes
if secsUntilFullFeed < 60 { if secsUntilFullFeed < 600 {
secsUntilFullFeed = 60 secsUntilFullFeed = 600
} }
return time.Duration(secsUntilFullFeed * int64(time.Second)) return time.Duration(secsUntilFullFeed) * time.Second
} }
func fullFeeder() { func fullFeeder(ctx context.Context) error {
for { fullFeedDuration := getFullFeedDuration()
for {
select { select {
case <-time.After(fullFeedDuration): case <-time.After(fullFeedDuration):
@ -39,11 +37,8 @@ func fullFeeder() {
} }
rngLock.Unlock() rngLock.Unlock()
case <-shutdownSignal: case <-ctx.Done():
return return nil
} }
fullFeedDuration = getFullFeedDuration()
} }
} }

View file

@ -6,19 +6,19 @@ import (
"io" "io"
"math" "math"
"time" "time"
)
"github.com/safing/portbase/config" const (
reseedAfterSeconds = 600 // ten minutes
reseedAfterBytes = 1048576 // one megabyte
) )
var ( var (
// Reader provides a global instance to read from the RNG. // Reader provides a global instance to read from the RNG.
Reader io.Reader Reader io.Reader
rngBytesRead int64 rngBytesRead uint64
rngLastFeed = time.Now() rngLastFeed = time.Now()
reseedAfterSeconds config.IntOption
reseedAfterBytes config.IntOption
) )
// reader provides an io.Reader interface // reader provides an io.Reader interface
@ -32,8 +32,8 @@ func checkEntropy() (err error) {
if !rngReady { if !rngReady {
return errors.New("RNG is not ready yet") return errors.New("RNG is not ready yet")
} }
if rngBytesRead > reseedAfterBytes() || if rngBytesRead > reseedAfterBytes ||
int64(time.Since(rngLastFeed).Seconds()) > reseedAfterSeconds() { int(time.Since(rngLastFeed).Seconds()) > reseedAfterSeconds {
select { select {
case r := <-rngFeeder: case r := <-rngFeeder:
rng.Reseed(r) rng.Reseed(r)

View file

@ -1,35 +1,36 @@
package rng package rng
import ( import (
"context"
"crypto/rand" "crypto/rand"
"time" "fmt"
"github.com/safing/portbase/log"
) )
func osFeeder() { func osFeeder(ctx context.Context) error {
entropyBytes := minFeedEntropy / 8
feeder := NewFeeder() feeder := NewFeeder()
defer feeder.CloseFeeder()
for { for {
// gather
// get feed entropy osEntropy := make([]byte, entropyBytes)
minEntropyBytes := int(minFeedEntropy())/8 + 1
if minEntropyBytes < 32 {
minEntropyBytes = 64
}
// get entropy
osEntropy := make([]byte, minEntropyBytes)
n, err := rand.Read(osEntropy) n, err := rand.Read(osEntropy)
if err != nil { if err != nil {
log.Errorf("could not read entropy from os: %s", err) return fmt.Errorf("could not read entropy from os: %s", err)
time.Sleep(10 * time.Second)
} }
if n != minEntropyBytes { if n != entropyBytes {
log.Errorf("could not read enough entropy from os: got only %d bytes instead of %d", n, minEntropyBytes) return fmt.Errorf("could not read enough entropy from os: got only %d bytes instead of %d", n, entropyBytes)
time.Sleep(10 * time.Second)
} }
// feed // feed
feeder.SupplyEntropy(osEntropy, minEntropyBytes*8) select {
case feeder.input <- &entropyData{
data: osEntropy,
entropy: entropyBytes * 8,
}:
case <-ctx.Done():
return nil
}
} }
} }

View file

@ -3,122 +3,60 @@ package rng
import ( import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"errors"
"fmt" "fmt"
"sync" "sync"
"github.com/aead/serpent" "github.com/aead/serpent"
"github.com/seehuhn/fortuna" "github.com/seehuhn/fortuna"
"github.com/safing/portbase/config"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
) )
var ( var (
rng *fortuna.Generator rng *fortuna.Generator
rngLock sync.Mutex rngLock sync.Mutex
rngReady = false rngReady = false
rngCipherOption config.StringOption
shutdownSignal = make(chan struct{}) rngCipher = "aes"
// possible values: aes, serpent
module *modules.Module
) )
func init() { func init() {
modules.Register("random", prep, Start, nil) module = modules.Register("random", nil, start, nil)
}
func prep() error {
err := config.Register(&config.Option{
Name: "RNG Cipher",
Key: "random/rng_cipher",
Description: "Cipher to use for the Fortuna RNG. Requires restart to take effect.",
OptType: config.OptTypeString,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
ExternalOptType: "string list",
DefaultValue: "aes",
ValidationRegex: "^(aes|serpent)$",
})
if err != nil {
return err
}
rngCipherOption = config.GetAsString("random/rng_cipher", "aes")
err = config.Register(&config.Option{
Name: "Minimum Feed Entropy",
Key: "random/min_feed_entropy",
Description: "The minimum amount of entropy before a entropy source is feed to the RNG, in bits.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: 256,
ValidationRegex: "^[0-9]{3,5}$",
})
if err != nil {
return err
}
minFeedEntropy = config.Concurrent.GetAsInt("random/min_feed_entropy", 256)
err = config.Register(&config.Option{
Name: "Reseed after x seconds",
Key: "random/reseed_after_seconds",
Description: "Number of seconds until reseed",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: 360, // ten minutes
ValidationRegex: "^[1-9][0-9]{1,5}$",
})
if err != nil {
return err
}
reseedAfterSeconds = config.Concurrent.GetAsInt("random/reseed_after_seconds", 360)
err = config.Register(&config.Option{
Name: "Reseed after x bytes",
Key: "random/reseed_after_bytes",
Description: "Number of fetched bytes until reseed",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: 1000000, // one megabyte
ValidationRegex: "^[1-9][0-9]{2,9}$",
})
if err != nil {
return err
}
reseedAfterBytes = config.GetAsInt("random/reseed_after_bytes", 1000000)
return nil
} }
func newCipher(key []byte) (cipher.Block, error) { func newCipher(key []byte) (cipher.Block, error) {
cipher := rngCipherOption() switch rngCipher {
switch cipher {
case "aes": case "aes":
return aes.NewCipher(key) return aes.NewCipher(key)
case "serpent": case "serpent":
return serpent.NewCipher(key) return serpent.NewCipher(key)
default: default:
return nil, fmt.Errorf("unknown or unsupported cipher: %s", cipher) return nil, fmt.Errorf("unknown or unsupported cipher: %s", rngCipher)
} }
} }
// Start starts the RNG. Normally, this should be only called by the portbase/modules package. func start() error {
func Start() (err error) {
rngLock.Lock() rngLock.Lock()
defer rngLock.Unlock() defer rngLock.Unlock()
rng = fortuna.NewGenerator(newCipher) rng = fortuna.NewGenerator(newCipher)
if rng == nil {
return errors.New("failed to initialize rng")
}
rngReady = true rngReady = true
// random source: OS // random source: OS
go osFeeder() module.StartServiceWorker("os rng feeder", 0, osFeeder)
// random source: goroutine ticks // random source: goroutine ticks
go tickFeeder() module.StartServiceWorker("tick rng feeder", 0, tickFeeder)
// full feeder // full feeder
go fullFeeder() module.StartServiceWorker("full feeder", 0, fullFeeder)
return nil return nil
} }

View file

@ -2,17 +2,10 @@ package rng
import ( import (
"testing" "testing"
"github.com/safing/portbase/config"
) )
func init() { func init() {
err := prep() err := start()
if err != nil {
panic(err)
}
err = Start()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -21,25 +14,17 @@ func init() {
func TestRNG(t *testing.T) { func TestRNG(t *testing.T) {
key := make([]byte, 16) key := make([]byte, 16)
err := config.SetConfigOption("random/rng_cipher", "aes") rngCipher = "aes"
if err != nil { _, err := newCipher(key)
t.Errorf("failed to set random/rng_cipher config: %s", err)
}
_, err = newCipher(key)
if err != nil { if err != nil {
t.Errorf("failed to create aes cipher: %s", err) t.Errorf("failed to create aes cipher: %s", err)
} }
rng.Reseed(key)
err = config.SetConfigOption("random/rng_cipher", "serpent") rngCipher = "serpent"
if err != nil {
t.Errorf("failed to set random/rng_cipher config: %s", err)
}
_, err = newCipher(key) _, err = newCipher(key)
if err != nil { if err != nil {
t.Errorf("failed to create serpent cipher: %s", err) t.Errorf("failed to create serpent cipher: %s", err)
} }
rng.Reseed(key)
b := make([]byte, 32) b := make([]byte, 32)
_, err = Read(b) _, err = Read(b)
@ -55,4 +40,9 @@ func TestRNG(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Bytes failed: %s", err) t.Errorf("Bytes failed: %s", err)
} }
_, err = Number(100)
if err != nil {
t.Errorf("Number failed: %s", err)
}
} }

View file

@ -1,27 +1,25 @@
package rng package rng
import ( import (
"context"
"encoding/binary"
"time" "time"
) )
var ( func getTickFeederTickDuration() time.Duration {
tickDuration = 1 * time.Millisecond
)
func getTickDuration() time.Duration {
// be ready in 1/10 time of reseedAfterSeconds // be ready in 1/10 time of reseedAfterSeconds
msecsAvailable := reseedAfterSeconds() * 100 msecsAvailable := reseedAfterSeconds * 100
// ex.: reseed after 10 minutes: msecsAvailable = 36000 // ex.: reseed after 10 minutes: msecsAvailable = 60000
// have full entropy after 5 minutes // have full entropy after 5 minutes
// one tick generates 0,125 bits of entropy // one tick generates 0,125 bits of entropy
ticksNeeded := minFeedEntropy() * 8 ticksNeeded := minFeedEntropy * 8
// ex.: minimum entropy is 256: ticksNeeded = 2048 // ex.: minimum entropy is 256: ticksNeeded = 2048
// msces between ticks // msces between ticks
tickMsecs := msecsAvailable / ticksNeeded tickMsecs := msecsAvailable / ticksNeeded
// ex.: tickMsecs = 17(,578125) // ex.: tickMsecs = 29(,296875)
// use a minimum of 10 msecs per tick for good entropy // use a minimum of 10 msecs per tick for good entropy
// it would take 21 seconds to get full 256 bits of entropy with 10msec ticks // it would take 21 seconds to get full 256 bits of entropy with 10msec ticks
@ -29,33 +27,51 @@ func getTickDuration() time.Duration {
tickMsecs = 10 tickMsecs = 10
} }
return time.Duration(tickMsecs * int64(time.Millisecond)) return time.Duration(tickMsecs) * time.Millisecond
} }
// tickFeeder is a really simple entropy feeder that adds the least significant bit of the current nanosecond unixtime to its pool every time it 'ticks'. // tickFeeder is a really simple entropy feeder that adds the least significant bit of the current nanosecond unixtime to its pool every time it 'ticks'.
// The more work the program does, the better the quality, as the internal schedular cannot immediately run the goroutine when it's ready. // The more work the program does, the better the quality, as the internal schedular cannot immediately run the goroutine when it's ready.
func tickFeeder() { func tickFeeder(ctx context.Context) error {
var value int64 var value int64
var pushes int var pushes int
feeder := NewFeeder() feeder := NewFeeder()
defer feeder.CloseFeeder()
tickDuration := getTickFeederTickDuration()
for { for {
select { // wait for tick
case <-time.After(tickDuration): time.Sleep(tickDuration)
value = (value << 1) | (time.Now().UnixNano() % 2) // add tick value
value = (value << 1) | (time.Now().UnixNano() % 2)
pushes++
pushes++ if pushes >= 64 {
if pushes >= 64 { // convert to []byte
feeder.SupplyEntropyAsInt(value, 8) b := make([]byte, 8)
pushes = 0 binary.LittleEndian.PutUint64(b, uint64(value))
// reset
pushes = 0
// feed
select {
case feeder.input <- &entropyData{
data: b,
entropy: 8,
}:
case <-ctx.Done():
return nil
}
} else {
// check if are done
select {
case <-ctx.Done():
return nil
default:
} }
tickDuration = getTickDuration()
case <-shutdownSignal:
return
} }
} }
} }