blob: 0c0792bb8a75714b20cfad4b90d7a4824efe7f82 [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() {
Serge Bazanski187c4bb2019-08-14 18:50:16 +0200144 if encryptor.stdout != nil {
145 encryptor.stdout.Close()
146 }
147 if encryptor.stderr != nil {
148 encryptor.stderr.Close()
149 }
150 if encryptor.stdin != nil {
151 encryptor.stdin.Close()
152 }
lb5tr716ecf62019-08-05 17:33:29 -0700153
154 os.RemoveAll(encryptor.tempDir)
155}
156
157func getGpgTempDir() (string, error) {
158 temp, err := ioutil.TempDir("", "gpg-")
159 if err != nil {
160 return "", fmt.Errorf("failed to create temporary gpg workspace %v", err)
161 }
162
163 return temp, nil
164}
165
166func cleanupProcess(cmd *exec.Cmd) {
167 if !cmd.ProcessState.Exited() {
168 cmd.Process.Kill()
169 }
170}
171
172func isOkExitCode(cmd *exec.Cmd) (bool, error) {
173 exitCode := cmd.ProcessState.ExitCode()
174
175 if exitCode == -1 {
176 return false, fmt.Errorf("process is either still runing or was terminated by a signal")
177 }
178
179 return exitCode == 0, nil
180}
181
182func makePipes(cmd *exec.Cmd) (stdout io.ReadCloser, stderr io.ReadCloser, stdin io.WriteCloser, error error) {
183 stdout, err := cmd.StdoutPipe()
184 if err != nil {
185 return nil, nil, nil, fmt.Errorf("cmd.StdoutPipe() failed %v", err)
186 }
187
188 stdin, err = cmd.StdinPipe()
189 if err != nil {
190 return nil, nil, nil, fmt.Errorf("cmd.StdinPipe() failed %v", err)
191 }
192
193 stderr, err = cmd.StderrPipe()
194 if err != nil {
195 return nil, nil, nil, fmt.Errorf("cmd.StderrPipe() failed %v", err)
196 }
197
198 return stdout, stderr, stdin, nil
199}