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  }