bgpwtf/cccampix/pgpencryptor: implement service

TODO:
  * tests

Change-Id: I5d0506542070236a8ee879fcb54bc9518e23b5e3
diff --git a/bgpwtf/cccampix/pgpencryptor/gpg/gpg.go b/bgpwtf/cccampix/pgpencryptor/gpg/gpg.go
new file mode 100644
index 0000000..09fcc7f
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/gpg/gpg.go
@@ -0,0 +1,193 @@
+package gpg
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"strings"
+	"time"
+)
+
+var ExecutionTimeLimit = 30 * time.Second
+var BinaryPath = "gpg"
+
+type Encryptor interface {
+	ReadCipherText(size int) ([]byte, error)
+	WritePlainText(chunk []byte) error
+	Finish()
+	Close()
+}
+
+type EncryptorFactory interface {
+	Get(recipient []byte, keyRing []byte) (Encryptor, error)
+}
+
+type CLIEncryptorFactory struct {
+}
+
+func (CLIEncryptorFactory) Get(recipient []byte, keyRing []byte) (Encryptor, error) {
+	return NewCLIEncryptor(recipient, keyRing)
+}
+
+type CLIEncryptor struct {
+	tempDir     string
+	recipient   []byte
+	cmd         *exec.Cmd
+	stdout      io.ReadCloser
+	stderr      io.ReadCloser
+	stdin       io.WriteCloser
+	initialized bool
+}
+
+func NewCLIEncryptor(recipient []byte, keyRing []byte) (*CLIEncryptor, error) {
+	temp, err := getGpgTempDir()
+	if err != nil {
+		return nil, err
+	}
+
+	keyRingTempPath := path.Join(temp, "_keyring")
+	err = ioutil.WriteFile(keyRingTempPath, keyRing, 0600)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO(lb5tr): test for command injection
+	importParams := []string{
+		"--homedir", temp,
+		"--import", keyRingTempPath,
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), ExecutionTimeLimit)
+	defer cancel()
+
+	cmd := exec.CommandContext(ctx, BinaryPath, importParams...)
+	err = cmd.Start()
+	if err != nil {
+		return nil, fmt.Errorf("failed to start import command: %v", err)
+	}
+
+	err = cmd.Wait()
+	if err != nil {
+		return nil, fmt.Errorf("workspace initialization failed: %v", err)
+	}
+
+	ok, err := isOkExitCode(cmd)
+	if !ok || err != nil {
+		return nil, fmt.Errorf("failed to initialize gpg workspace due to gpg execution failure")
+	}
+
+	// spawn background encryption process
+	// TODO(lb5tr): test for command injection
+	encryptParams := []string{
+		"--encrypt",
+		"--homedir", temp,
+		"--recipient", strings.ToUpper(hex.EncodeToString(recipient)),
+		"--trust-model", "always",
+		"--yes",
+		"--batch",
+	}
+
+	cmd = exec.Command(BinaryPath, encryptParams...)
+	stdout, stderr, stdin, err := makePipes(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	err = cmd.Start()
+	if err != nil {
+		return nil, fmt.Errorf("failed to start encryptor process: %v", err)
+	}
+
+	encryptor := CLIEncryptor{
+		tempDir:     temp,
+		recipient:   recipient,
+		cmd:         cmd,
+		stdin:       stdin,
+		stderr:      stderr,
+		stdout:      stdout,
+		initialized: true,
+	}
+
+	return &encryptor, nil
+}
+
+func (encryptor *CLIEncryptor) WritePlainText(chunk []byte) error {
+	if !encryptor.initialized {
+		return fmt.Errorf("encryptor is not initialized")
+	}
+
+	encryptor.stdin.Write(chunk)
+	return nil
+}
+
+func (encryptor *CLIEncryptor) ReadCipherText(size int) ([]byte, error) {
+	if !encryptor.initialized {
+		return nil, fmt.Errorf("encryptor is not initialized")
+	}
+
+	buf := make([]byte, size)
+	n, err := encryptor.stdout.Read(buf)
+
+	return buf[:n], err
+}
+
+func (encryptor *CLIEncryptor) Finish() {
+	encryptor.stdin.Close()
+}
+
+func (encryptor *CLIEncryptor) Close() {
+	encryptor.stdout.Close()
+	encryptor.stderr.Close()
+	encryptor.stdin.Close()
+
+	os.RemoveAll(encryptor.tempDir)
+}
+
+func getGpgTempDir() (string, error) {
+	temp, err := ioutil.TempDir("", "gpg-")
+	if err != nil {
+		return "", fmt.Errorf("failed to create temporary gpg workspace %v", err)
+	}
+
+	return temp, nil
+}
+
+func cleanupProcess(cmd *exec.Cmd) {
+	if !cmd.ProcessState.Exited() {
+		cmd.Process.Kill()
+	}
+}
+
+func isOkExitCode(cmd *exec.Cmd) (bool, error) {
+	exitCode := cmd.ProcessState.ExitCode()
+
+	if exitCode == -1 {
+		return false, fmt.Errorf("process is either still runing or was terminated by a signal")
+	}
+
+	return exitCode == 0, nil
+}
+
+func makePipes(cmd *exec.Cmd) (stdout io.ReadCloser, stderr io.ReadCloser, stdin io.WriteCloser, error error) {
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("cmd.StdoutPipe() failed %v", err)
+	}
+
+	stdin, err = cmd.StdinPipe()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("cmd.StdinPipe() failed %v", err)
+	}
+
+	stderr, err = cmd.StderrPipe()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("cmd.StderrPipe() failed %v", err)
+	}
+
+	return stdout, stderr, stdin, nil
+}