1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package libkb
5
6import (
7	"bytes"
8	"errors"
9	"fmt"
10	"io"
11	"os"
12	"os/exec"
13	"strings"
14	"sync"
15
16	"github.com/blang/semver"
17)
18
19type GpgCLI struct {
20	Contextified
21	path    string
22	options []string
23	version string
24	tty     string
25
26	mutex *sync.Mutex
27
28	logUI LogUI
29}
30
31func NewGpgCLI(g *GlobalContext, logUI LogUI) *GpgCLI {
32	if logUI == nil {
33		logUI = g.Log
34	}
35	return &GpgCLI{
36		Contextified: NewContextified(g),
37		mutex:        new(sync.Mutex),
38		logUI:        logUI,
39	}
40}
41
42func (g *GpgCLI) SetTTY(t string) {
43	g.tty = t
44}
45
46func (g *GpgCLI) Configure(mctx MetaContext) (err error) {
47
48	g.mutex.Lock()
49	defer g.mutex.Unlock()
50
51	prog := g.G().Env.GetGpg()
52	opts := g.G().Env.GetGpgOptions()
53
54	if len(prog) > 0 {
55		err = canExec(prog)
56	} else {
57		prog, err = exec.LookPath("gpg2")
58		if err != nil {
59			prog, err = exec.LookPath("gpg")
60		}
61	}
62	if err != nil {
63		return err
64	}
65
66	mctx.Debug("| configured GPG w/ path: %s", prog)
67
68	g.path = prog
69	g.options = opts
70
71	return
72}
73
74// CanExec returns true if a gpg executable exists.
75func (g *GpgCLI) CanExec(mctx MetaContext) (bool, error) {
76	err := g.Configure(mctx)
77	if IsExecError(err) {
78		return false, nil
79	}
80	if err != nil {
81		return false, err
82	}
83	return true, nil
84}
85
86// Path returns the path of the gpg executable.
87// Path is only available if CanExec() is true.
88func (g *GpgCLI) Path(mctx MetaContext) string {
89	canExec, err := g.CanExec(mctx)
90	if err == nil && canExec {
91		return g.path
92	}
93	return ""
94}
95
96func (g *GpgCLI) ImportKeyArmored(mctx MetaContext, secret bool, fp PGPFingerprint, tty string) (string, error) {
97	g.outputVersion(mctx)
98	var cmd string
99	var which string
100	if secret {
101		cmd = "--export-secret-key"
102		which = "secret "
103	} else {
104		cmd = "--export"
105	}
106
107	arg := RunGpg2Arg{
108		Arguments: []string{"--armor", cmd, fp.String()},
109		Stdout:    true,
110		TTY:       tty,
111	}
112
113	res := g.Run2(mctx, arg)
114	if res.Err != nil {
115		return "", res.Err
116	}
117
118	buf := new(bytes.Buffer)
119	_, err := buf.ReadFrom(res.Stdout)
120	if err != nil {
121		return "", err
122	}
123	armored := buf.String()
124
125	// Convert to posix style on windows
126	armored = PosixLineEndings(armored)
127
128	if err := res.Wait(); err != nil {
129		return "", err
130	}
131
132	if len(armored) == 0 {
133		return "", NoKeyError{fmt.Sprintf("No %skey found for fingerprint %s", which, fp)}
134	}
135
136	return armored, nil
137}
138
139func (g *GpgCLI) ImportKey(mctx MetaContext, secret bool, fp PGPFingerprint, tty string) (*PGPKeyBundle, error) {
140
141	armored, err := g.ImportKeyArmored(mctx, secret, fp, tty)
142	if err != nil {
143		return nil, err
144	}
145
146	bundle, w, err := ReadOneKeyFromString(armored)
147	w.Warn(g.G())
148	if err != nil {
149		return nil, err
150	}
151
152	// For secret keys, *also* import the key in public mode, and then grab the
153	// ArmoredPublicKey from that. That's because the public import goes out of
154	// its way to preserve the exact armored string from GPG.
155	if secret {
156		publicBundle, err := g.ImportKey(mctx, false, fp, tty)
157		if err != nil {
158			return nil, err
159		}
160		bundle.ArmoredPublicKey = publicBundle.ArmoredPublicKey
161
162		// It's a bug that gpg --export-secret-keys doesn't grep subkey revocations.
163		// No matter, we have both in-memory, so we can copy it over here
164		bundle.CopySubkeyRevocations(publicBundle.Entity)
165	}
166
167	return bundle, nil
168}
169
170func (g *GpgCLI) ExportKeyArmored(mctx MetaContext, s string) (err error) {
171	g.outputVersion(mctx)
172	arg := RunGpg2Arg{
173		Arguments: []string{"--import"},
174		Stdin:     true,
175	}
176	res := g.Run2(mctx, arg)
177	if res.Err != nil {
178		return res.Err
179	}
180	_, err = res.Stdin.Write([]byte(s))
181	if err != nil {
182		return err
183	}
184	err = res.Stdin.Close()
185	if err != nil {
186		return err
187	}
188	err = res.Wait()
189	return err
190}
191
192func (g *GpgCLI) ExportKey(mctx MetaContext, k PGPKeyBundle, private bool, batch bool) (err error) {
193	g.outputVersion(mctx)
194	arg := RunGpg2Arg{
195		Arguments: []string{"--import"},
196		Stdin:     true,
197	}
198
199	if batch {
200		arg.Arguments = append(arg.Arguments, "--batch")
201	}
202
203	res := g.Run2(mctx, arg)
204	if res.Err != nil {
205		return res.Err
206	}
207
208	e1 := k.EncodeToStream(res.Stdin, private)
209	e2 := res.Stdin.Close()
210	e3 := res.Wait()
211	return PickFirstError(e1, e2, e3)
212}
213
214func (g *GpgCLI) Sign(mctx MetaContext, fp PGPFingerprint, payload []byte) (string, error) {
215	g.outputVersion(mctx)
216	arg := RunGpg2Arg{
217		Arguments: []string{"--armor", "--sign", "-u", fp.String()},
218		Stdout:    true,
219		Stdin:     true,
220	}
221
222	res := g.Run2(mctx, arg)
223	if res.Err != nil {
224		return "", res.Err
225	}
226
227	_, err := res.Stdin.Write(payload)
228	if err != nil {
229		return "", err
230	}
231	res.Stdin.Close()
232
233	buf := new(bytes.Buffer)
234	_, err = buf.ReadFrom(res.Stdout)
235	if err != nil {
236		return "", err
237	}
238	armored := buf.String()
239
240	// Convert to posix style on windows
241	armored = PosixLineEndings(armored)
242
243	if err := res.Wait(); err != nil {
244		return "", err
245	}
246
247	return armored, nil
248}
249
250func (g *GpgCLI) Version() (string, error) {
251	if len(g.version) > 0 {
252		return g.version, nil
253	}
254
255	args := append(g.options, "--version")
256	out, err := exec.Command(g.path, args...).Output()
257	if err != nil {
258		return "", err
259	}
260	g.version = string(out)
261	return g.version, nil
262}
263
264func (g *GpgCLI) outputVersion(mctx MetaContext) {
265	v, err := g.Version()
266	if err != nil {
267		mctx.Debug("error getting GPG version: %s", err)
268		return
269	}
270	mctx.Debug("GPG version:\n%s", v)
271}
272
273func (g *GpgCLI) SemanticVersion() (*semver.Version, error) {
274	out, err := g.Version()
275	if err != nil {
276		return nil, err
277	}
278	lines := strings.Split(out, "\n")
279	if len(lines) == 0 {
280		return nil, errors.New("empty gpg version")
281	}
282	parts := strings.Fields(lines[0])
283	if len(parts) < 3 {
284		return nil, fmt.Errorf("unhandled gpg version output %q full: %q", lines[0], lines)
285	}
286	return semver.New(parts[2])
287}
288
289func (g *GpgCLI) VersionAtLeast(s string) (bool, error) {
290	min, err := semver.New(s)
291	if err != nil {
292		return false, err
293	}
294	cur, err := g.SemanticVersion()
295	if err != nil {
296		return false, err
297	}
298	return cur.GTE(*min), nil
299}
300
301type RunGpg2Arg struct {
302	Arguments []string
303	Stdin     bool
304	Stderr    bool
305	Stdout    bool
306	TTY       string
307}
308
309type RunGpg2Res struct {
310	Stdin  io.WriteCloser
311	Stdout io.ReadCloser
312	Stderr io.ReadCloser
313	Wait   func() error
314	Err    error
315}
316
317func (g *GpgCLI) Run2(mctx MetaContext, arg RunGpg2Arg) (res RunGpg2Res) {
318	if g.path == "" {
319		res.Err = errors.New("no gpg path set")
320		return
321	}
322
323	cmd := g.MakeCmd(mctx, arg.Arguments, arg.TTY)
324
325	if arg.Stdin {
326		if res.Stdin, res.Err = cmd.StdinPipe(); res.Err != nil {
327			return
328		}
329	}
330
331	var stdout, stderr io.ReadCloser
332
333	if stdout, res.Err = cmd.StdoutPipe(); res.Err != nil {
334		return
335	}
336	if stderr, res.Err = cmd.StderrPipe(); res.Err != nil {
337		return
338	}
339
340	if res.Err = cmd.Start(); res.Err != nil {
341		return
342	}
343
344	waited := false
345	out := 0
346	ch := make(chan error)
347	var fep FirstErrorPicker
348
349	res.Wait = func() error {
350		for out > 0 {
351			fep.Push(<-ch)
352			out--
353		}
354		if !waited {
355			waited = true
356			err := cmd.Wait()
357			if err != nil {
358				fep.Push(ErrorToGpgError(err))
359			}
360			return fep.Error()
361		}
362		return nil
363	}
364
365	bgmctx := mctx.BackgroundWithLogTags()
366	if !arg.Stdout {
367		out++
368		go func() {
369			ch <- DrainPipe(stdout, func(s string) { bgmctx.Debug(s) })
370		}()
371	} else {
372		res.Stdout = stdout
373	}
374
375	if !arg.Stderr {
376		out++
377		go func() {
378			ch <- DrainPipe(stderr, func(s string) { bgmctx.Debug(s) })
379		}()
380	} else {
381		res.Stderr = stderr
382	}
383
384	return
385}
386
387func (g *GpgCLI) MakeCmd(mctx MetaContext, args []string, tty string) *exec.Cmd {
388	var nargs []string
389	if g.options != nil {
390		nargs = make([]string, len(g.options))
391		copy(nargs, g.options)
392		nargs = append(nargs, args...)
393	} else {
394		nargs = args
395	}
396	// Always use --no-auto-check-trustdb to prevent gpg from refreshing trustdb.
397	// Refreshing the trustdb can cause hangs when bad keys from CVE-2019-13050 are in the keyring.
398	// --no-auto-check-trustdb was introduced around gpg 1.0 so ought to always be implemented.
399	nargs = append([]string{"--no-auto-check-trustdb"}, nargs...)
400	if g.G().Service {
401		nargs = append([]string{"--no-tty"}, nargs...)
402	}
403	mctx.Debug("| running Gpg: %s %s", g.path, strings.Join(nargs, " "))
404	ret := exec.Command(g.path, nargs...)
405	if tty == "" {
406		tty = g.tty
407	}
408	if tty != "" {
409		ret.Env = append(os.Environ(), "GPG_TTY="+tty)
410		mctx.Debug("| setting GPG_TTY=%s", tty)
411	} else {
412		mctx.Debug("| no tty provided, GPG_TTY will not be changed")
413	}
414	return ret
415}
416