goldwarden-vaultwarden-bitw.../cli/agent/systemauth/pinentry/keybase-pinentry/pinentry.go

219 lines
4.7 KiB
Go

// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
package pinentry
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/keybase/client/go/logger"
keybase1 "github.com/keybase/client/go/protocol/keybase1"
)
//
// some borrowed from here:
//
// https://github.com/bradfitz/camlistore/blob/master/pkg/misc/pinentry/pinentry.go
//
// Under the Apache 2.0 license
//
type Pinentry struct {
initRes *error
path string
term string
tty string
prog string
log logger.Logger
}
func New(envprog string, log logger.Logger, tty string) *Pinentry {
return &Pinentry{
prog: envprog,
log: log,
tty: tty,
}
}
func (pe *Pinentry) Init() (error, error) {
if pe.initRes != nil {
return *pe.initRes, nil
}
err, fatalerr := pe.FindProgram()
if err == nil {
pe.GetTerminalName()
}
pe.term = os.Getenv("TERM")
pe.initRes = &err
return err, fatalerr
}
func (pe *Pinentry) SetInitError(e error) {
pe.initRes = &e
}
func (pe *Pinentry) FindProgram() (error, error) {
prog := pe.prog
var err, fatalerr error
if len(prog) > 0 {
if err = canExec(prog); err == nil {
pe.path = prog
} else {
err = fmt.Errorf("Can't execute given pinentry program '%s': %s",
prog, err)
fatalerr = err
}
} else if prog, err = FindPinentry(pe.log); err == nil {
pe.path = prog
}
return err, fatalerr
}
func (pe *Pinentry) Get(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) {
pe.log.Debug("+ Pinentry::Get()")
// Do a lazy initialization
if err, _ = pe.Init(); err != nil {
return
}
inst := pinentryInstance{parent: pe}
defer inst.Close()
if err = inst.Init(); err != nil {
// We probably shouldn't try to use this thing again if we failed
// to set it up.
pe.SetInitError(err)
return
}
res, err = inst.Run(arg)
pe.log.Debug("- Pinentry::Get() -> %v", err)
return
}
func (pi *pinentryInstance) Close() {
pi.stdin.Close()
_ = pi.cmd.Wait()
}
type pinentryInstance struct {
parent *Pinentry
cmd *exec.Cmd
stdout io.ReadCloser
stdin io.WriteCloser
br *bufio.Reader
}
func (pi *pinentryInstance) Set(cmd, val string, errp *error) {
if val == "" {
return
}
fmt.Fprintf(pi.stdin, "%s %s\n", cmd, val)
line, _, err := pi.br.ReadLine()
if err != nil {
*errp = err
return
}
if string(line) != "OK" {
*errp = fmt.Errorf("Response to " + cmd + " was " + string(line))
}
}
func (pi *pinentryInstance) Init() (err error) {
parent := pi.parent
parent.log.Debug("+ pinentryInstance::Init()")
pi.cmd = exec.Command(parent.path)
pi.stdin, _ = pi.cmd.StdinPipe()
pi.stdout, _ = pi.cmd.StdoutPipe()
if err = pi.cmd.Start(); err != nil {
parent.log.Warning("unexpected error running pinentry (%s): %s", parent.path, err)
return
}
pi.br = bufio.NewReader(pi.stdout)
lineb, _, err := pi.br.ReadLine()
if err != nil {
err = fmt.Errorf("Failed to get getpin greeting: %s", err)
return
}
line := string(lineb)
if !strings.HasPrefix(line, "OK") {
err = fmt.Errorf("getpin greeting didn't say 'OK', said: %q", line)
return
}
if len(parent.tty) > 0 {
parent.log.Debug("setting ttyname to %s", parent.tty)
pi.Set("OPTION", "ttyname="+parent.tty, &err)
if err != nil {
parent.log.Debug("error setting ttyname: %s", err)
}
}
if len(parent.term) > 0 {
parent.log.Debug("setting ttytype to %s", parent.term)
pi.Set("OPTION", "ttytype="+parent.term, &err)
if err != nil {
parent.log.Debug("error setting ttytype: %s", err)
}
}
parent.log.Debug("- pinentryInstance::Init() -> %v", err)
return
}
func descEncode(s string) string {
s = strings.Replace(s, "%", "%%", -1)
s = strings.Replace(s, "\n", "%0A", -1)
return s
}
func resDecode(s string) string {
s = strings.Replace(s, "%25", "%", -1)
return s
}
func (pi *pinentryInstance) Run(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) {
pi.Set("SETPROMPT", arg.Prompt, &err)
pi.Set("SETDESC", descEncode(arg.Desc), &err)
pi.Set("SETOK", arg.Ok, &err)
pi.Set("SETCANCEL", arg.Cancel, &err)
pi.Set("SETERROR", arg.Err, &err)
if err != nil {
return
}
fmt.Fprintf(pi.stdin, "GETPIN\n")
var lineb []byte
lineb, _, err = pi.br.ReadLine()
if err != nil {
err = fmt.Errorf("Failed to read line after GETPIN: %v", err)
return
}
line := string(lineb)
switch {
case strings.HasPrefix(line, "D "):
res = &keybase1.SecretEntryRes{Text: resDecode(line[2:])}
case strings.HasPrefix(line, "ERR 83886179 canceled") || strings.HasPrefix(line, "ERR 83886179 Operation cancelled"):
res = &keybase1.SecretEntryRes{Canceled: true}
case line == "OK":
res = &keybase1.SecretEntryRes{}
default:
return nil, fmt.Errorf("GETPIN response didn't start with D; got %q", line)
}
return
}