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 }