github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/pinentry/pinentry.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 pinentry 5 6 import ( 7 "bufio" 8 "fmt" 9 "io" 10 "os" 11 "os/exec" 12 "strings" 13 14 "github.com/keybase/client/go/logger" 15 keybase1 "github.com/keybase/client/go/protocol/keybase1" 16 ) 17 18 // 19 // some borrowed from here: 20 // 21 // https://github.com/bradfitz/camlistore/blob/master/pkg/misc/pinentry/pinentry.go 22 // 23 // Under the Apache 2.0 license 24 // 25 26 type Pinentry struct { 27 initRes *error 28 path string 29 term string 30 tty string 31 prog string 32 log logger.Logger 33 } 34 35 func New(envprog string, log logger.Logger, tty string) *Pinentry { 36 return &Pinentry{ 37 prog: envprog, 38 log: log, 39 tty: tty, 40 } 41 } 42 43 func (pe *Pinentry) Init() (error, error) { 44 if pe.initRes != nil { 45 return *pe.initRes, nil 46 } 47 err, fatalerr := pe.FindProgram() 48 if err == nil { 49 pe.GetTerminalName() 50 } 51 pe.term = os.Getenv("TERM") 52 pe.initRes = &err 53 return err, fatalerr 54 } 55 56 func (pe *Pinentry) SetInitError(e error) { 57 pe.initRes = &e 58 } 59 60 func (pe *Pinentry) FindProgram() (error, error) { 61 prog := pe.prog 62 var err, fatalerr error 63 if len(prog) > 0 { 64 if err = canExec(prog); err == nil { 65 pe.path = prog 66 } else { 67 err = fmt.Errorf("Can't execute given pinentry program '%s': %s", 68 prog, err) 69 fatalerr = err 70 } 71 } else if prog, err = FindPinentry(pe.log); err == nil { 72 pe.path = prog 73 } 74 return err, fatalerr 75 } 76 77 func (pe *Pinentry) Get(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) { 78 79 pe.log.Debug("+ Pinentry::Get()") 80 81 // Do a lazy initialization 82 if err, _ = pe.Init(); err != nil { 83 return 84 } 85 86 inst := pinentryInstance{parent: pe} 87 defer inst.Close() 88 89 if err = inst.Init(); err != nil { 90 // We probably shouldn't try to use this thing again if we failed 91 // to set it up. 92 pe.SetInitError(err) 93 return 94 } 95 res, err = inst.Run(arg) 96 pe.log.Debug("- Pinentry::Get() -> %v", err) 97 return 98 } 99 100 func (pi *pinentryInstance) Close() { 101 pi.stdin.Close() 102 _ = pi.cmd.Wait() 103 } 104 105 type pinentryInstance struct { 106 parent *Pinentry 107 cmd *exec.Cmd 108 stdout io.ReadCloser 109 stdin io.WriteCloser 110 br *bufio.Reader 111 } 112 113 func (pi *pinentryInstance) Set(cmd, val string, errp *error) { 114 if val == "" { 115 return 116 } 117 fmt.Fprintf(pi.stdin, "%s %s\n", cmd, val) 118 line, _, err := pi.br.ReadLine() 119 if err != nil { 120 *errp = err 121 return 122 } 123 if string(line) != "OK" { 124 *errp = fmt.Errorf("Response to " + cmd + " was " + string(line)) 125 } 126 } 127 128 func (pi *pinentryInstance) Init() (err error) { 129 parent := pi.parent 130 131 parent.log.Debug("+ pinentryInstance::Init()") 132 133 pi.cmd = exec.Command(parent.path) 134 pi.stdin, _ = pi.cmd.StdinPipe() 135 pi.stdout, _ = pi.cmd.StdoutPipe() 136 137 if err = pi.cmd.Start(); err != nil { 138 parent.log.Warning("unexpected error running pinentry (%s): %s", parent.path, err) 139 return 140 } 141 142 pi.br = bufio.NewReader(pi.stdout) 143 lineb, _, err := pi.br.ReadLine() 144 145 if err != nil { 146 err = fmt.Errorf("Failed to get getpin greeting: %s", err) 147 return 148 } 149 150 line := string(lineb) 151 if !strings.HasPrefix(line, "OK") { 152 err = fmt.Errorf("getpin greeting didn't say 'OK', said: %q", line) 153 return 154 } 155 156 if len(parent.tty) > 0 { 157 parent.log.Debug("setting ttyname to %s", parent.tty) 158 pi.Set("OPTION", "ttyname="+parent.tty, &err) 159 if err != nil { 160 parent.log.Debug("error setting ttyname: %s", err) 161 } 162 } 163 if len(parent.term) > 0 { 164 parent.log.Debug("setting ttytype to %s", parent.term) 165 pi.Set("OPTION", "ttytype="+parent.term, &err) 166 if err != nil { 167 parent.log.Debug("error setting ttytype: %s", err) 168 } 169 } 170 171 parent.log.Debug("- pinentryInstance::Init() -> %v", err) 172 return 173 } 174 175 func descEncode(s string) string { 176 s = strings.ReplaceAll(s, "%", "%%") 177 s = strings.ReplaceAll(s, "\n", "%0A") 178 return s 179 } 180 181 func resDecode(s string) string { 182 s = strings.ReplaceAll(s, "%25", "%") 183 return s 184 } 185 186 func (pi *pinentryInstance) Run(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) { 187 188 pi.Set("SETPROMPT", arg.Prompt, &err) 189 pi.Set("SETDESC", descEncode(arg.Desc), &err) 190 pi.Set("SETOK", arg.Ok, &err) 191 pi.Set("SETCANCEL", arg.Cancel, &err) 192 pi.Set("SETERROR", arg.Err, &err) 193 194 if err != nil { 195 return 196 } 197 198 fmt.Fprintf(pi.stdin, "GETPIN\n") 199 var lineb []byte 200 lineb, _, err = pi.br.ReadLine() 201 if err != nil { 202 err = fmt.Errorf("Failed to read line after GETPIN: %v", err) 203 return 204 } 205 line := string(lineb) 206 switch { 207 case strings.HasPrefix(line, "D "): 208 res = &keybase1.SecretEntryRes{Text: resDecode(line[2:])} 209 case strings.HasPrefix(line, "ERR 83886179 canceled") || strings.HasPrefix(line, "ERR 83886179 Operation cancelled"): 210 res = &keybase1.SecretEntryRes{Canceled: true} 211 case line == "OK": 212 res = &keybase1.SecretEntryRes{} 213 default: 214 return nil, fmt.Errorf( 215 "failed to run pinentry: GETPIN response didn't start with D; got %q (see %s for troubleshooting help)", 216 line, 217 "https://github.com/keybase/client/blob/master/go/doc/troubleshooting.md#pinentry-doesnt-work", 218 ) 219 } 220 221 return 222 }