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