// Copyright (c) 2019, Google Inc.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

// Package subprocess contains functionality to talk to a modulewrapper for
// testing of various algorithm implementations.
package subprocess

import (
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"
)

// Transactable provides an interface to allow test injection of transactions
// that don't call a server.
type Transactable interface {
	Transact(cmd string, expectedResults int, args ...[]byte) ([][]byte, error)
	TransactAsync(cmd string, expectedResults int, args [][]byte, callback func([][]byte) error)
	Barrier(callback func()) error
	Flush() error
}

// Subprocess is a "middle" layer that interacts with a FIPS module via running
// a command and speaking a simple protocol over stdin/stdout.
type Subprocess struct {
	cmd        *exec.Cmd
	stdin      io.WriteCloser
	stdout     io.ReadCloser
	primitives map[string]primitive
	// supportsFlush is true if the modulewrapper indicated that it wants to receive flush commands.
	supportsFlush bool
	// pendingReads is a queue of expected responses. `readerRoutine` reads each response and calls the callback in the matching pendingRead.
	pendingReads chan pendingRead
	// readerFinished is a channel that is closed if `readerRoutine` has finished (e.g. because of a read error).
	readerFinished chan struct{}
}

// pendingRead represents an expected response from the modulewrapper.
type pendingRead struct {
	// barrierCallback is called as soon as this pendingRead is the next in the queue, before any read from the modulewrapper.
	barrierCallback func()

	// callback is called with the result from the modulewrapper. If this is nil then no read is performed.
	callback func(result [][]byte) error
	// cmd is the command that requested this read for logging purposes.
	cmd                string
	expectedNumResults int
}

// New returns a new Subprocess middle layer that runs the given binary.
func New(path string, arg ...string) (*Subprocess, error) {
	cmd := exec.Command(path, arg...)
	cmd.Stderr = os.Stderr
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, err
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}

	if err := cmd.Start(); err != nil {
		return nil, err
	}

	return NewWithIO(cmd, stdin, stdout), nil
}

// maxPending is the maximum number of requests that can be in the pipeline.
const maxPending = 4096

// NewWithIO returns a new Subprocess middle layer with the given ReadCloser and
// WriteCloser. The returned Subprocess will call Wait on the Cmd when closed.
func NewWithIO(cmd *exec.Cmd, in io.WriteCloser, out io.ReadCloser) *Subprocess {
	m := &Subprocess{
		cmd:            cmd,
		stdin:          in,
		stdout:         out,
		pendingReads:   make(chan pendingRead, maxPending),
		readerFinished: make(chan struct{}),
	}

	m.primitives = map[string]primitive{
		"SHA-1":        &hashPrimitive{"SHA-1", 20},
		"SHA2-224":     &hashPrimitive{"SHA2-224", 28},
		"SHA2-256":     &hashPrimitive{"SHA2-256", 32},
		"SHA2-384":     &hashPrimitive{"SHA2-384", 48},
		"SHA2-512":     &hashPrimitive{"SHA2-512", 64},
		"SHA2-512/224": &hashPrimitive{"SHA2-512/224", 28},
		"SHA2-512/256": &hashPrimitive{"SHA2-512/256", 32},
		"SHA3-224":     &hashPrimitive{"SHA3-224", 28},
		"SHA3-256":     &hashPrimitive{"SHA3-256", 32},
		"SHA3-384":     &hashPrimitive{"SHA3-384", 48},
		"SHA3-512":     &hashPrimitive{"SHA3-512", 64},
		// NOTE: SHAKE does not have a predifined digest output size
		"SHAKE-128":         &hashPrimitive{"SHAKE-128", -1},
		"SHAKE-256":         &hashPrimitive{"SHAKE-256", -1},
		"ACVP-AES-ECB":      &blockCipher{"AES", 16, 2, true, false, iterateAES},
		"ACVP-AES-CBC":      &blockCipher{"AES-CBC", 16, 2, true, true, iterateAESCBC},
		"ACVP-AES-CBC-CS3":  &blockCipher{"AES-CBC-CS3", 16, 1, false, true, iterateAESCBC},
		"ACVP-AES-CTR":      &blockCipher{"AES-CTR", 16, 1, false, true, nil},
		"ACVP-TDES-ECB":     &blockCipher{"3DES-ECB", 8, 3, true, false, iterate3DES},
		"ACVP-TDES-CBC":     &blockCipher{"3DES-CBC", 8, 3, true, true, iterate3DESCBC},
		"ACVP-AES-XTS":      &xts{},
		"ACVP-AES-GCM":      &aead{"AES-GCM", false},
		"ACVP-AES-GMAC":     &aead{"AES-GCM", false},
		"ACVP-AES-CCM":      &aead{"AES-CCM", true},
		"ACVP-AES-KW":       &aead{"AES-KW", false},
		"ACVP-AES-KWP":      &aead{"AES-KWP", false},
		"HMAC-SHA-1":        &hmacPrimitive{"HMAC-SHA-1", 20},
		"HMAC-SHA2-224":     &hmacPrimitive{"HMAC-SHA2-224", 28},
		"HMAC-SHA2-256":     &hmacPrimitive{"HMAC-SHA2-256", 32},
		"HMAC-SHA2-384":     &hmacPrimitive{"HMAC-SHA2-384", 48},
		"HMAC-SHA2-512":     &hmacPrimitive{"HMAC-SHA2-512", 64},
		"HMAC-SHA2-512/224": &hmacPrimitive{"HMAC-SHA2-512/224", 28},
		"HMAC-SHA2-512/256": &hmacPrimitive{"HMAC-SHA2-512/256", 32},
		"HMAC-SHA3-224":     &hmacPrimitive{"HMAC-SHA3-224", 28},
		"HMAC-SHA3-256":     &hmacPrimitive{"HMAC-SHA3-256", 32},
		"HMAC-SHA3-384":     &hmacPrimitive{"HMAC-SHA3-384", 48},
		"HMAC-SHA3-512":     &hmacPrimitive{"HMAC-SHA3-512", 64},
		"ctrDRBG":           &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}},
		"hmacDRBG":          &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true}},
		"KDF":               &kdfPrimitive{},
		"KDA": &kdaPrimitive{
			modes: map[string]kdaModePrimitive{
				"HKDF":    &kdaHkdfMode{},
				"OneStep": &kdaOneStepMode{},
			},
		},
		"TLS-v1.3":       &tls13{},
		"CMAC-AES":       &keyedMACPrimitive{"CMAC-AES"},
		"RSA":            &rsa{},
		"kdf-components": &kdfComp{"kdf-components"},
		"TLS-v1.2":       &kdfComp{"TLS-v1.2"},
		"KAS-ECC-SSC":    &kas{},
		"KAS-FFC-SSC":    &kasDH{},
		"PBKDF":          &pbkdf{},
		"ML-KEM":         &mlKem{},
		"EDDSA":          &eddsa{},
		"ML-DSA":         &mlDsa{},
	}
	m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}

	go m.readerRoutine()
	return m
}

// Close signals the child process to exit and waits for it to complete.
func (m *Subprocess) Close() {
	m.stdout.Close()
	m.stdin.Close()
	m.cmd.Wait()
	close(m.pendingReads)
	<-m.readerFinished
}

func (m *Subprocess) flush() error {
	if !m.supportsFlush {
		return nil
	}

	const cmd = "flush"
	buf := make([]byte, 8, 8+len(cmd))
	binary.LittleEndian.PutUint32(buf, 1)
	binary.LittleEndian.PutUint32(buf[4:], uint32(len(cmd)))
	buf = append(buf, []byte(cmd)...)

	if _, err := m.stdin.Write(buf); err != nil {
		return err
	}
	return nil
}

func (m *Subprocess) enqueueRead(pending pendingRead) error {
	select {
	case <-m.readerFinished:
		panic("attempted to enqueue request after the reader failed")
	default:
	}

	select {
	case m.pendingReads <- pending:
		break
	default:
		// `pendingReads` is full. Ensure that the modulewrapper will process
		// some outstanding requests to free up space in the queue.
		if err := m.flush(); err != nil {
			return err
		}
		m.pendingReads <- pending
	}

	return nil
}

func (m *Subprocess) PID() int {
	return m.cmd.Process.Pid
}

// TransactAsync performs a single request--response pair with the subprocess.
// The callback will run at some future point, in a separate goroutine. All
// callbacks will, however, be run in the order that TransactAsync was called.
// Use Flush to wait for all outstanding callbacks.
func (m *Subprocess) TransactAsync(cmd string, expectedNumResults int, args [][]byte, callback func(result [][]byte) error) {
	if err := m.enqueueRead(pendingRead{nil, callback, cmd, expectedNumResults}); err != nil {
		panic(err)
	}

	argLength := len(cmd)
	for _, arg := range args {
		argLength += len(arg)
	}

	buf := make([]byte, 4*(2+len(args)), 4*(2+len(args))+argLength)
	binary.LittleEndian.PutUint32(buf, uint32(1+len(args)))
	binary.LittleEndian.PutUint32(buf[4:], uint32(len(cmd)))
	for i, arg := range args {
		binary.LittleEndian.PutUint32(buf[4*(i+2):], uint32(len(arg)))
	}
	buf = append(buf, []byte(cmd)...)
	for _, arg := range args {
		buf = append(buf, arg...)
	}

	if _, err := m.stdin.Write(buf); err != nil {
		panic(err)
	}
}

// Flush tells the subprocess to complete all outstanding requests and waits
// for all outstanding TransactAsync callbacks to complete.
func (m *Subprocess) Flush() error {
	if m.supportsFlush {
		m.flush()
	}

	done := make(chan struct{})
	if err := m.enqueueRead(pendingRead{barrierCallback: func() {
		close(done)
	}}); err != nil {
		return err
	}

	<-done
	return nil
}

// Barrier runs callback after all outstanding TransactAsync callbacks have
// been run.
func (m *Subprocess) Barrier(callback func()) error {
	return m.enqueueRead(pendingRead{barrierCallback: callback})
}

func (m *Subprocess) Transact(cmd string, expectedNumResults int, args ...[]byte) ([][]byte, error) {
	done := make(chan struct{})
	var result [][]byte
	m.TransactAsync(cmd, expectedNumResults, args, func(r [][]byte) error {
		result = r
		close(done)
		return nil
	})

	if err := m.flush(); err != nil {
		return nil, err
	}

	select {
	case <-done:
		return result, nil
	case <-m.readerFinished:
		panic("was still waiting for a result when the reader finished")
	}
}

func (m *Subprocess) readerRoutine() {
	defer close(m.readerFinished)

	for pendingRead := range m.pendingReads {
		if pendingRead.barrierCallback != nil {
			pendingRead.barrierCallback()
		}

		if pendingRead.callback == nil {
			continue
		}

		result, err := m.readResult(pendingRead.cmd, pendingRead.expectedNumResults)
		if err != nil {
			panic(fmt.Errorf("failed to read from subprocess: %w", err))
		}

		if err := pendingRead.callback(result); err != nil {
			panic(fmt.Errorf("result from subprocess was rejected: %w", err))
		}
	}
}

func (m *Subprocess) readResult(cmd string, expectedNumResults int) ([][]byte, error) {
	buf := make([]byte, 4)

	if _, err := io.ReadFull(m.stdout, buf); err != nil {
		return nil, err
	}

	numResults := binary.LittleEndian.Uint32(buf)
	if int(numResults) != expectedNumResults {
		return nil, fmt.Errorf("expected %d results from %q but got %d", expectedNumResults, cmd, numResults)
	}

	buf = make([]byte, 4*numResults)
	if _, err := io.ReadFull(m.stdout, buf); err != nil {
		return nil, err
	}

	var resultsLength uint64
	for i := uint32(0); i < numResults; i++ {
		resultsLength += uint64(binary.LittleEndian.Uint32(buf[4*i:]))
	}

	if resultsLength > (1 << 30) {
		return nil, fmt.Errorf("results too large (%d bytes)", resultsLength)
	}

	results := make([]byte, resultsLength)
	if _, err := io.ReadFull(m.stdout, results); err != nil {
		return nil, err
	}

	ret := make([][]byte, 0, numResults)
	var offset int
	for i := uint32(0); i < numResults; i++ {
		length := binary.LittleEndian.Uint32(buf[4*i:])
		ret = append(ret, results[offset:offset+int(length)])
		offset += int(length)
	}

	return ret, nil
}

// Config returns a JSON blob that describes the supported primitives. The
// format of the blob is defined by ACVP. See
// http://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#rfc.section.11.15.2.1
func (m *Subprocess) Config() ([]byte, error) {
	results, err := m.Transact("getConfig", 1)
	if err != nil {
		return nil, err
	}
	var config []struct {
		Algorithm string   `json:"algorithm"`
		Features  []string `json:"features"`
	}
	if err := json.Unmarshal(results[0], &config); err != nil {
		return nil, errors.New("failed to parse config response from wrapper: " + err.Error())
	}
	for _, algo := range config {
		if algo.Algorithm == "acvptool" {
			for _, feature := range algo.Features {
				switch feature {
				case "batch":
					m.supportsFlush = true
				}
			}
		} else if _, ok := m.primitives[algo.Algorithm]; !ok {
			return nil, fmt.Errorf("wrapper config advertises support for unknown algorithm %q", algo.Algorithm)
		}
	}

	return results[0], nil
}

// Process runs a set of test vectors and returns the result.
func (m *Subprocess) Process(algorithm string, vectorSet []byte) (interface{}, error) {
	prim, ok := m.primitives[algorithm]
	if !ok {
		return nil, fmt.Errorf("unknown algorithm %q", algorithm)
	}
	ret, err := prim.Process(vectorSet, m)
	if err != nil {
		return nil, err
	}
	return ret, nil
}

type primitive interface {
	Process(vectorSet []byte, t Transactable) (interface{}, error)
}

func uint32le(n uint32) []byte {
	var ret [4]byte
	binary.LittleEndian.PutUint32(ret[:], n)
	return ret[:]
}

type hexEncodedByteString []byte

func (h *hexEncodedByteString) MarshalJSON() ([]byte, error) {
	if h == nil || *h == nil {
		return []byte("null"), nil
	}
	hexStr := strings.ToUpper(hex.EncodeToString(*h))
	return json.Marshal(hexStr)
}

func (h *hexEncodedByteString) UnmarshalJSON(jsonBytes []byte) error {
	var jsonValue interface{}
	if err := json.Unmarshal(jsonBytes, &jsonValue); err != nil {
		return err
	}

	switch v := jsonValue.(type) {
	case string:
		if len(v) == 0 {
			*h = nil
			return nil
		}
		bv, err := hex.DecodeString(v)
		if err != nil {
			return err
		}
		*h = bv
	case nil:
		*h = nil
	default:
		return fmt.Errorf("HexEncodedBytes doesn't support json type: %T", jsonValue)
	}

	return nil
}
