diff --git a/README.md b/README.md index 49dcf6a..dbe46df 100644 --- a/README.md +++ b/README.md @@ -12,5 +12,5 @@ This is still fairly immature. Here's what I want to be able to do with it: - [x] print the ID (`cmdI`) - [x] verify the image (`cmdX` & verify PGP signature) - [x] generate some amount of entropy (`onerng read` command) -- [ ] add extra AES128-whitening +- [x] add extra AES128-whitening - [ ] run as a daemon and integrate with `rngd` diff --git a/cmd/read.go b/cmd/read.go index 556099f..861a378 100644 --- a/cmd/read.go +++ b/cmd/read.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "io" "os" "time" @@ -18,6 +19,8 @@ func readCmd(ctx context.Context) *cobra.Command { disableAvalanche := false enableRF := false disableWhitener := false + enableAESWhiten := true + count := int64(-1) cmd := &cobra.Command{ Use: "read", @@ -28,16 +31,9 @@ func readCmd(ctx context.Context) *cobra.Command { if err != nil { return errors.Wrapf(err, "init failed") } - var out *os.File - if readOut == "-" { - out = os.Stdout - } else { - out, err = os.OpenFile(readOut, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - return err - } - } - flags := onerng.ReadMode(onerng.Default) + + // set flags based on commandline options + flags := onerng.Default if disableAvalanche { flags |= onerng.DisableAvalanche } @@ -47,6 +43,34 @@ func readCmd(ctx context.Context) *cobra.Command { if disableWhitener { flags |= onerng.DisableWhitener } + + // waste some entropy... + devNull, err := os.OpenFile("/dev/null", os.O_WRONLY, 0200) + if err != nil { + return err + } + _, err = o.Read(ctx, devNull, 10*1024, flags) + if err != nil { + fmt.Fprintf(os.Stderr, "warning: entropy wasteage failed or incomplete, continuing anyway\n") + } + + var out io.WriteCloser + if readOut == "-" { + out = os.Stdout + } else { + out, err = os.OpenFile(readOut, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return err + } + } + + if enableAESWhiten { + out, err = o.AESWhitener(ctx, out) + if err != nil { + return err + } + } + start := time.Now() written, err := o.Read(ctx, out, count, flags) delta := time.Since(start) @@ -60,5 +84,6 @@ func readCmd(ctx context.Context) *cobra.Command { cmd.Flags().BoolVar(&enableRF, "enable-rf", false, "Enable noise generation from RF") cmd.Flags().BoolVar(&disableWhitener, "disable-whitener", false, "Disable the on-board CRC16 generator") cmd.Flags().Int64VarP(&count, "count", "n", -1, "Read only N bytes (use -1 for unlimited)") + cmd.Flags().BoolVar(&enableAESWhiten, "aes-whitener", true, "encrypt with AES-128 to 'whiten' the input stream with a random key obtained from the OneRNG") return cmd } diff --git a/copy.go b/copy.go index 47d9ce2..db2107e 100644 --- a/copy.go +++ b/copy.go @@ -18,7 +18,7 @@ func copyWithContext(ctx context.Context, dst io.Writer, src *os.File, n int64) allowedTimeouts := 10 rf := func(p []byte) (int, error) { - // we don't want reads to block forever, but we also don't want to time out immediately + // I don't want reads to block forever, but I also don't want to time out immediately err := src.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) if err != nil { return 0, err diff --git a/onerng.go b/onerng.go index 8170125..d0589d5 100644 --- a/onerng.go +++ b/onerng.go @@ -2,9 +2,13 @@ package onerng import ( "bufio" + "bytes" "context" + "crypto/aes" + "crypto/cipher" "fmt" "io" + mrand "math/rand" "os" "strconv" "strings" @@ -15,15 +19,20 @@ import ( // OneRNG - a OneRNG device type OneRNG struct { - Path string + Path string + device *os.File } // ReadMode - type ReadMode uint32 -func (o *OneRNG) cmd(ctx context.Context, d *os.File, c ...string) (err error) { +func (o *OneRNG) cmd(ctx context.Context, c ...string) error { + err := o.open() + if err != nil { + return err + } for _, v := range c { - _, err = d.WriteString(v) + _, err = o.device.WriteString(v) if err != nil { return errors.Wrapf(err, "Errored on command %s", v) } @@ -36,15 +45,32 @@ func (o *OneRNG) cmd(ctx context.Context, d *os.File, c ...string) (err error) { return nil } +func (o *OneRNG) open() (err error) { + if o.device != nil { + return nil + } + o.device, err = os.OpenFile(o.Path, os.O_RDWR, 0600) + return err +} + +func (o *OneRNG) close() error { + if o.device == nil { + return nil + } + err := o.device.Close() + o.device = nil + return err +} + // Version - func (o *OneRNG) Version(ctx context.Context) (int, error) { - d, err := os.OpenFile(o.Path, os.O_RDWR, 0600) + err := o.open() if err != nil { return 0, err } - defer d.Close() + defer o.close() - err = o.cmd(ctx, d, CmdPause) + err = o.cmd(ctx, CmdPause) if err != nil { return 0, err } @@ -53,9 +79,9 @@ func (o *OneRNG) Version(ctx context.Context) (int, error) { defer cancel() buf := make(chan string) errc := make(chan error, 1) - go scan(ctx, d, buf, errc) + go o.scan(ctx, buf, errc) - err = o.cmd(ctx, d, CmdSilent, CmdVersion, CmdRun) + err = o.cmd(ctx, CmdSilent, CmdVersion, CmdRun) if err != nil { return 0, err } @@ -78,7 +104,7 @@ loop: } } - err = o.cmd(ctx, d, CmdPause) + err = o.cmd(ctx, CmdPause) if err != nil { return 0, err } @@ -90,19 +116,19 @@ loop: // Identify - func (o *OneRNG) Identify(ctx context.Context) (string, error) { - d, err := os.OpenFile(o.Path, os.O_RDWR, 0600) + err := o.open() if err != nil { return "", err } - defer d.Close() + defer o.close() _, cancel := context.WithCancel(ctx) defer cancel() buf := make(chan string) errc := make(chan error, 1) - go scan(ctx, d, buf, errc) + go o.scan(ctx, buf, errc) - err = o.cmd(ctx, d, CmdSilent, CmdID, CmdRun) + err = o.cmd(ctx, CmdSilent, CmdID, CmdRun) if err != nil { return "", err } @@ -125,7 +151,7 @@ loop: } } - err = o.cmd(ctx, d, CmdPause) + err = o.cmd(ctx, CmdPause) if err != nil { return "", err } @@ -135,27 +161,27 @@ loop: // Flush - func (o *OneRNG) Flush(ctx context.Context) error { - d, err := os.OpenFile(o.Path, os.O_RDWR, 0600) + err := o.open() if err != nil { return err } - defer d.Close() + defer o.close() _, cancel := context.WithCancel(ctx) defer cancel() - err = o.cmd(ctx, d, CmdFlush) + err = o.cmd(ctx, CmdFlush) return err } // Image - func (o *OneRNG) Image(ctx context.Context) ([]byte, error) { - d, err := os.OpenFile(o.Path, os.O_RDWR, 0600) + err := o.open() if err != nil { return nil, err } - defer d.Close() + defer o.close() - err = o.cmd(ctx, d, CmdPause, CmdSilent) + err = o.cmd(ctx, CmdPause, CmdSilent) if err != nil { return nil, err } @@ -166,9 +192,9 @@ func (o *OneRNG) Image(ctx context.Context) ([]byte, error) { defer cancel() buf := make(chan []byte) errc := make(chan error, 1) - go stream(ctx, d, 4, buf, errc) + go o.stream(ctx, 4, buf, errc) - err = o.cmd(ctx, d, CmdSilent, CmdImage, CmdRun) + err = o.cmd(ctx, CmdSilent, CmdImage, CmdRun) if err != nil { return nil, err } @@ -198,7 +224,7 @@ loop: } } - err = o.cmd(ctx, d, CmdPause) + err = o.cmd(ctx, CmdPause) if err != nil { return nil, err } @@ -224,45 +250,45 @@ func (o *OneRNG) Init(ctx context.Context) error { // Read - func (o *OneRNG) Read(ctx context.Context, out io.WriteCloser, n int64, flags ReadMode) (written int64, err error) { - d, err := os.OpenFile(o.Path, os.O_RDWR, 0600) + err = o.open() if err != nil { return 0, err } - defer d.Close() + defer o.close() - err = o.cmd(ctx, d, NoiseCommand(flags), CmdRun) + err = o.cmd(ctx, NoiseCommand(flags), CmdRun) if err != nil { return 0, err } - defer o.cmd(ctx, d, CmdPause) + defer o.cmd(ctx, CmdPause) - written, err = copyWithContext(ctx, out, d, n) + written, err = copyWithContext(ctx, out, o.device, n) return written, err } // readData - try to read some data from the RNG func (o *OneRNG) readData(ctx context.Context) (int, error) { - d, err := os.OpenFile(o.Path, os.O_RDWR, 0600) + err := o.open() if err != nil { return 0, err } - defer d.Close() + defer o.close() _, cancel := context.WithTimeout(ctx, 50*time.Millisecond) defer cancel() buf := make(chan []byte) errc := make(chan error, 1) - go stream(ctx, d, 1, buf, errc) + go o.stream(ctx, 1, buf, errc) - err = o.cmd(ctx, d, CmdAvalanche, CmdRun) + err = o.cmd(ctx, CmdAvalanche, CmdRun) if err != nil { return 0, err } // make sure we always end with a pause/silence/flush - defer o.cmd(ctx, d, CmdPause, CmdSilent, CmdFlush) + defer o.cmd(ctx, CmdPause, CmdSilent, CmdFlush) // blocking read from the channel, with a timeout (from context) select { @@ -277,12 +303,18 @@ func (o *OneRNG) readData(ctx context.Context) (int, error) { // stream from a file into a channel until an error is encountered, the channel // is closed, or the context is cancelled. -func stream(ctx context.Context, d *os.File, bs int, buf chan []byte, errc chan error) { +func (o *OneRNG) stream(ctx context.Context, bs int, buf chan []byte, errc chan error) { + err := o.open() + if err != nil { + errc <- err + return + } + defer close(buf) defer close(errc) for { b := make([]byte, bs) - n, err := io.ReadAtLeast(d, b, len(b)) + n, err := io.ReadAtLeast(o.device, b, len(b)) if err != nil { errc <- err return @@ -300,10 +332,15 @@ func stream(ctx context.Context, d *os.File, bs int, buf chan []byte, errc chan } } -func scan(ctx context.Context, d *os.File, buf chan string, errc chan error) { +func (o *OneRNG) scan(ctx context.Context, buf chan string, errc chan error) { + err := o.open() + if err != nil { + errc <- err + return + } defer close(buf) defer close(errc) - scanner := bufio.NewScanner(d) + scanner := bufio.NewScanner(o.device) for scanner.Scan() { select { case <-ctx.Done(): @@ -318,3 +355,55 @@ func NoiseCommand(flags ReadMode) string { num := strconv.Itoa(int(flags)) return "cmd" + num + "\n" } + +type aesWhitener struct { + out io.WriteCloser +} + +// AESWhitener creates a "whitener" that wraps the provided writer. The random +// data that the OneRNG generates is sometimes a little "too" random for some +// purposes (i.e. rngd), so this can be used to further mangle that data in non- +// predictable ways. +// +// This uses AES-128. +func (o *OneRNG) AESWhitener(ctx context.Context, out io.WriteCloser) (io.WriteCloser, error) { + k, err := o.key(ctx) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(k) + if err != nil { + return nil, err + } + + // create a random IV with math/rand - doesn't need to be cryptographically-random + iv := make([]byte, aes.BlockSize) + _, err = mrand.Read(iv) + if err != nil { + return nil, err + } + + stream := cipher.NewCFBEncrypter(block, iv) + s := &cipher.StreamWriter{S: stream, W: out} + return s, nil +} + +func (o *OneRNG) key(ctx context.Context) ([]byte, error) { + err := o.open() + if err != nil { + return []byte{}, err + } + + buf := &bytes.Buffer{} + + err = o.cmd(ctx, CmdAvalanche, CmdRun) + if err != nil { + return []byte{}, err + } + defer o.cmd(ctx, CmdPause) + // 16 bytes == AES-128 + _, err = copyWithContext(ctx, buf, o.device, 16) + k := buf.Bytes() + + return k, err +}