blob: 09fcc7fac15fe52d55c5458f0586539e9a12ca7b [file] [log] [blame]
lb5tr716ecf62019-08-05 17:33:29 -07001package gpg
2
3import (
4 "context"
5 "encoding/hex"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "os"
10 "os/exec"
11 "path"
12 "strings"
13 "time"
14)
15
16var ExecutionTimeLimit = 30 * time.Second
17var BinaryPath = "gpg"
18
19type Encryptor interface {
20 ReadCipherText(size int) ([]byte, error)
21 WritePlainText(chunk []byte) error
22 Finish()
23 Close()
24}
25
26type EncryptorFactory interface {
27 Get(recipient []byte, keyRing []byte) (Encryptor, error)
28}
29
30type CLIEncryptorFactory struct {
31}
32
33func (CLIEncryptorFactory) Get(recipient []byte, keyRing []byte) (Encryptor, error) {
34 return NewCLIEncryptor(recipient, keyRing)
35}
36
37type CLIEncryptor struct {
38 tempDir string
39 recipient []byte
40 cmd *exec.Cmd
41 stdout io.ReadCloser
42 stderr io.ReadCloser
43 stdin io.WriteCloser
44 initialized bool
45}
46
47func NewCLIEncryptor(recipient []byte, keyRing []byte) (*CLIEncryptor, error) {
48 temp, err := getGpgTempDir()
49 if err != nil {
50 return nil, err
51 }
52
53 keyRingTempPath := path.Join(temp, "_keyring")
54 err = ioutil.WriteFile(keyRingTempPath, keyRing, 0600)
55 if err != nil {
56 return nil, err
57 }
58
59 // TODO(lb5tr): test for command injection
60 importParams := []string{
61 "--homedir", temp,
62 "--import", keyRingTempPath,
63 }
64
65 ctx, cancel := context.WithTimeout(context.Background(), ExecutionTimeLimit)
66 defer cancel()
67
68 cmd := exec.CommandContext(ctx, BinaryPath, importParams...)
69 err = cmd.Start()
70 if err != nil {
71 return nil, fmt.Errorf("failed to start import command: %v", err)
72 }
73
74 err = cmd.Wait()
75 if err != nil {
76 return nil, fmt.Errorf("workspace initialization failed: %v", err)
77 }
78
79 ok, err := isOkExitCode(cmd)
80 if !ok || err != nil {
81 return nil, fmt.Errorf("failed to initialize gpg workspace due to gpg execution failure")
82 }
83
84 // spawn background encryption process
85 // TODO(lb5tr): test for command injection
86 encryptParams := []string{
87 "--encrypt",
88 "--homedir", temp,
89 "--recipient", strings.ToUpper(hex.EncodeToString(recipient)),
90 "--trust-model", "always",
91 "--yes",
92 "--batch",
93 }
94
95 cmd = exec.Command(BinaryPath, encryptParams...)
96 stdout, stderr, stdin, err := makePipes(cmd)
97 if err != nil {
98 return nil, err
99 }
100
101 err = cmd.Start()
102 if err != nil {
103 return nil, fmt.Errorf("failed to start encryptor process: %v", err)
104 }
105
106 encryptor := CLIEncryptor{
107 tempDir: temp,
108 recipient: recipient,
109 cmd: cmd,
110 stdin: stdin,
111 stderr: stderr,
112 stdout: stdout,
113 initialized: true,
114 }
115
116 return &encryptor, nil
117}
118
119func (encryptor *CLIEncryptor) WritePlainText(chunk []byte) error {
120 if !encryptor.initialized {
121 return fmt.Errorf("encryptor is not initialized")
122 }
123
124 encryptor.stdin.Write(chunk)
125 return nil
126}
127
128func (encryptor *CLIEncryptor) ReadCipherText(size int) ([]byte, error) {
129 if !encryptor.initialized {
130 return nil, fmt.Errorf("encryptor is not initialized")
131 }
132
133 buf := make([]byte, size)
134 n, err := encryptor.stdout.Read(buf)
135
136 return buf[:n], err
137}
138
139func (encryptor *CLIEncryptor) Finish() {
140 encryptor.stdin.Close()
141}
142
143func (encryptor *CLIEncryptor) Close() {
144 encryptor.stdout.Close()
145 encryptor.stderr.Close()
146 encryptor.stdin.Close()
147
148 os.RemoveAll(encryptor.tempDir)
149}
150
151func getGpgTempDir() (string, error) {
152 temp, err := ioutil.TempDir("", "gpg-")
153 if err != nil {
154 return "", fmt.Errorf("failed to create temporary gpg workspace %v", err)
155 }
156
157 return temp, nil
158}
159
160func cleanupProcess(cmd *exec.Cmd) {
161 if !cmd.ProcessState.Exited() {
162 cmd.Process.Kill()
163 }
164}
165
166func isOkExitCode(cmd *exec.Cmd) (bool, error) {
167 exitCode := cmd.ProcessState.ExitCode()
168
169 if exitCode == -1 {
170 return false, fmt.Errorf("process is either still runing or was terminated by a signal")
171 }
172
173 return exitCode == 0, nil
174}
175
176func makePipes(cmd *exec.Cmd) (stdout io.ReadCloser, stderr io.ReadCloser, stdin io.WriteCloser, error error) {
177 stdout, err := cmd.StdoutPipe()
178 if err != nil {
179 return nil, nil, nil, fmt.Errorf("cmd.StdoutPipe() failed %v", err)
180 }
181
182 stdin, err = cmd.StdinPipe()
183 if err != nil {
184 return nil, nil, nil, fmt.Errorf("cmd.StdinPipe() failed %v", err)
185 }
186
187 stderr, err = cmd.StderrPipe()
188 if err != nil {
189 return nil, nil, nil, fmt.Errorf("cmd.StderrPipe() failed %v", err)
190 }
191
192 return stdout, stderr, stdin, nil
193}