github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/gpg_cli.go (about)

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