/*
Package onerng provides functions to help interface with the OneRNG hardware RNG.

See http://onerng.info for information about the device, and see especially
http://www.moonbaseotago.com/onerng/theory.html for the theory of operation.

To use this package, you must first plug the OneRNG into an available USB port,
and your OS should auto-detect the device as a USB serial modem. On Linux, you
may need to load the cdc_acm module.

Once you know which device file points to the OneRNG, you can instantiate a
*OneRNG struct instance. All communication with the OneRNG is done through
this instance.

	o := &OneRNG{Path: "/dev/ttyACM0"}
	version, err := o.Version(context.TODO())
	if err != nil {
		return err
	}
	fmt.Printf("version is %d\n", version)

Reading data from the OneRNG can be done with the Read function:

	o := &OneRNG{Path: "/dev/ttyACM0"}
	_, err = o.Read(context.TODO(), os.Stdout, -1, EnableRF | DisableWhitener)
	if err != nil {
		return err
	}
*/
package onerng

import (
	"bufio"
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"io"
	mrand "math/rand"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
)

// OneRNG - a OneRNG device
type OneRNG struct {
	Path   string
	device io.ReadWriteCloser
}

// cmd sends one or more commands to the OneRNG. The device is not closed on
// completion, as it's usually being read from simultaneously.
func (o *OneRNG) cmd(ctx context.Context, c ...string) error {
	err := o.open()
	if err != nil {
		return err
	}
	for _, v := range c {
		_, err = o.device.Write([]byte(v))
		if err != nil {
			return errors.Wrapf(err, "Errored on command %q", v)
		}
		select {
		case <-ctx.Done():
			return nil
		default:
		}
	}
	return nil
}

// open the OneRNG device for read/write, if it hasn't already been opened.
// Access it as o.device.
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
}

// close the OneRNG device if it hasn't already been closed
func (o *OneRNG) close() error {
	if o.device == nil {
		return nil
	}
	err := o.device.Close()
	o.device = nil
	return err
}

// Version - query the OneRNG for its hardware version
func (o *OneRNG) Version(ctx context.Context) (int, error) {
	err := o.open()
	if err != nil {
		return 0, err
	}
	defer o.close()

	err = o.cmd(ctx, cmdPause)
	if err != nil {
		return 0, err
	}

	_, cancel := context.WithCancel(ctx)
	defer cancel()
	buf := make(chan string)
	errc := make(chan error, 1)
	go o.scan(ctx, buf, errc)

	err = o.cmd(ctx, noiseCommand(Silent), cmdVersion, cmdRun)
	if err != nil {
		return 0, err
	}

	verString := ""
loop:
	for {
		var b string
		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		case b = <-buf:
			if strings.HasPrefix(b, "Version ") {
				verString = b
				cancel()
				break loop
			}
		case err := <-errc:
			return 0, err
		}
	}

	err = o.cmd(ctx, cmdPause)
	if err != nil {
		return 0, err
	}

	n := strings.Replace(verString, "Version ", "", 1)
	version, err := strconv.Atoi(n)
	return version, err
}

// Identify - query the OneRNG for its ID
func (o *OneRNG) Identify(ctx context.Context) (string, error) {
	err := o.open()
	if err != nil {
		return "", err
	}
	defer o.close()

	_, cancel := context.WithCancel(ctx)
	defer cancel()
	buf := make(chan string)
	errc := make(chan error, 1)
	go o.scan(ctx, buf, errc)

	err = o.cmd(ctx, noiseCommand(Silent), cmdID, cmdRun)
	if err != nil {
		return "", err
	}

	idString := ""
loop:
	for {
		var b string
		select {
		case <-ctx.Done():
			return "", ctx.Err()
		case b = <-buf:
			if strings.HasPrefix(b, "___") {
				idString = b
				cancel()
				break loop
			}
		case err := <-errc:
			return "", err
		}
	}

	err = o.cmd(ctx, cmdPause)
	if err != nil {
		return "", err
	}

	return idString, err
}

// Flush the OneRNG's entropy pool
func (o *OneRNG) Flush(ctx context.Context) error {
	err := o.open()
	if err != nil {
		return err
	}
	defer o.close()

	_, cancel := context.WithCancel(ctx)
	defer cancel()
	err = o.cmd(ctx, cmdFlush)
	return err
}

// Image extracts the firmware image. This image is padded with random data to
// either 128Kb or 256Kb (depending on hardware), and signed.
//
// See also the Verify function.
func (o *OneRNG) Image(ctx context.Context) ([]byte, error) {
	err := o.open()
	if err != nil {
		return nil, err
	}
	defer o.close()

	err = o.cmd(ctx, cmdPause, noiseCommand(Silent))
	if err != nil {
		return nil, err
	}

	time.Sleep(2 * time.Second)

	_, cancel := context.WithCancel(ctx)
	defer cancel()
	buf := make(chan []byte)
	errc := make(chan error, 1)
	go o.stream(ctx, 4, buf, errc)

	err = o.cmd(ctx, noiseCommand(Silent), cmdImage, cmdRun)
	if err != nil {
		return nil, err
	}

	image := []byte{}
	zeros := 0
loop:
	for {
		var b []byte
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case b = <-buf:
			image = append(image, b...)
			for _, v := range b {
				if v == 0 {
					zeros++
				} else {
					zeros = 0
				}
				if zeros > 200 {
					break loop
				}
			}
		case err := <-errc:
			return nil, err
		}
	}

	err = o.cmd(ctx, cmdPause)
	if err != nil {
		return nil, err
	}

	return image, err
}

// Init - wait for the device to finish initializing and start returning data
func (o *OneRNG) Init(ctx context.Context) error {
	i := 0
	for ; i < 200; i++ {
		n, err := o.readData(ctx)
		if err != nil {
			return err
		}
		if n > 0 {
			break
		}
	}
	// fmt.Fprintf(os.Stderr, "Initialized after %d loops\n", i)
	return nil
}

// Read n bytes of data from the OneRNG into the given Writer. Set flags to
// configure the OneRNG's. Set n to -1 to continuously read until an error is
// encountered, or the context is cancelled.
//
// The OneRNG device will be closed when the operation completes.
func (o *OneRNG) Read(ctx context.Context, out io.Writer, n int64, flags NoiseMode) (written int64, err error) {
	err = o.open()
	if err != nil {
		return 0, err
	}
	defer o.close()

	err = o.cmd(ctx, noiseCommand(flags), cmdRun)
	if err != nil {
		return 0, err
	}

	defer o.cmd(ctx, cmdPause)

	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) {
	err := o.open()
	if err != nil {
		return 0, err
	}
	defer o.close()

	_, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
	defer cancel()

	buf := make(chan []byte)
	errc := make(chan error, 1)
	go o.stream(ctx, 1, buf, errc)

	err = o.cmd(ctx, noiseCommand(Default), cmdRun)
	if err != nil {
		return 0, err
	}

	// make sure we always end with a pause/silence/flush
	defer o.cmd(ctx, cmdPause, noiseCommand(Silent), cmdFlush)

	// blocking read from the channel, with a timeout (from context)
	select {
	case <-ctx.Done():
		return 0, ctx.Err()
	case b := <-buf:
		return len(b), nil
	case err := <-errc:
		return 0, err
	}
}

// stream from a file into a channel until an error is encountered, the channel
// is closed, or the context is cancelled.
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(o.device, b, len(b))
		if err != nil {
			errc <- err
			return
		}
		if n < len(b) {
			errc <- errors.Errorf("unexpected short read - wanted %db, read %db", len(b), n)
			return
		}

		select {
		case <-ctx.Done():
			return
		case buf <- b:
		}
	}
}

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(o.device)
	for scanner.Scan() {
		select {
		case <-ctx.Done():
			return
		case buf <- scanner.Text():
		}
	}
}

const (
	// cmdVersion - print firmware version (as "Version n")
	cmdVersion = "cmdv\n"
	// cmdFlush - flush entropy pool
	cmdFlush = "cmdw\n"
	// cmdImage - extract the signed firmware image for verification
	cmdImage = "cmdX\n"
	// cmdID - print hardware ID
	cmdID = "cmdI\n"
	// cmdRun - start the task
	cmdRun = "cmdO\n"
	// cmdPause - stop/pause the task
	cmdPause = "cmdo\n"
)

// 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, noiseCommand(Default), 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
}

// NoiseMode represents the different noise-generation modes available to the OneRNG
type NoiseMode uint32

const (
	// DisableWhitener - Disable the on-board CRC16 generator - no effect if both noise generators are disabled
	DisableWhitener NoiseMode = 1 << iota
	// EnableRF - Enable noise generation from RF
	EnableRF
	// DisableAvalanche - Disable noise generation from the Avalanche Diode
	DisableAvalanche

	// Default mode - Avalanche enabled, RF disabled, Whitener enabled.
	Default NoiseMode = 0
	// Silent - a convenience - everything disabled
	Silent NoiseMode = 4
)

// noiseCommand converts the given mode to the appropriate command to send to the OneRNG
func noiseCommand(flags NoiseMode) string {
	num := strconv.Itoa(int(flags))
	return "cmd" + num + "\n"
}

type readerFunc func(p []byte) (n int, err error)

func (rf readerFunc) Read(p []byte) (n int, err error) { return rf(p) }

// io.CopyN/io.Copy with cancellation support
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader, n int64) (int64, error) {
	// allow 10 500ms timeouts, for a total of 5s. After this, it's probably worth just giving up
	allowedTimeouts := 10

	rf := func(p []byte) (int, error) {
		if f, ok := src.(*os.File); ok {
			// I don't want reads to block forever, but I also don't want to time out immediately
			err := f.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
			if err != nil {
				return 0, err
			}
		}

		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		default:
			n, err := src.Read(p)
			if allowedTimeouts > 0 {
				if err != nil && os.IsTimeout(err) {
					allowedTimeouts--
					return n, nil
				}
			}
			return n, err
		}
	}

	if n < 0 {
		return io.Copy(dst, readerFunc(rf))
	}
	return io.CopyN(dst, readerFunc(rf), n)
}
