bgpwtf/cccampix/pgpencryptor: implement service
TODO:
* tests
Change-Id: I5d0506542070236a8ee879fcb54bc9518e23b5e3
diff --git a/bgpwtf/cccampix/pgpencryptor/gpg/BUILD.bazel b/bgpwtf/cccampix/pgpencryptor/gpg/BUILD.bazel
new file mode 100644
index 0000000..dbca7db
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/gpg/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["gpg.go"],
+ importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/gpg",
+ visibility = ["//visibility:public"],
+)
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
+}