blob: 09fcc7fac15fe52d55c5458f0586539e9a12ca7b [file] [log] [blame]
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
}