1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package pinentry
5
6import (
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
26type Pinentry struct {
27	initRes *error
28	path    string
29	term    string
30	tty     string
31	prog    string
32	log     logger.Logger
33}
34
35func New(envprog string, log logger.Logger, tty string) *Pinentry {
36	return &Pinentry{
37		prog: envprog,
38		log:  log,
39		tty:  tty,
40	}
41}
42
43func (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
56func (pe *Pinentry) SetInitError(e error) {
57	pe.initRes = &e
58}
59
60func (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
77func (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
100func (pi *pinentryInstance) Close() {
101	pi.stdin.Close()
102	_ = pi.cmd.Wait()
103}
104
105type pinentryInstance struct {
106	parent *Pinentry
107	cmd    *exec.Cmd
108	stdout io.ReadCloser
109	stdin  io.WriteCloser
110	br     *bufio.Reader
111}
112
113func (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
128func (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
175func descEncode(s string) string {
176	s = strings.Replace(s, "%", "%%", -1)
177	s = strings.Replace(s, "\n", "%0A", -1)
178	return s
179}
180
181func resDecode(s string) string {
182	s = strings.Replace(s, "%25", "%", -1)
183	return s
184}
185
186func (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}
223