pkg/prompter/pinentry.go (83 lines of code) (raw):
package prompter
import (
"bufio"
"fmt"
"io"
"os/exec"
"strings"
"sync"
)
const (
defaultPinentryDialog string = "Security token [%s]"
)
// PinentryRunner is the interface for pinentry to run itself
type PinentryRunner interface {
Run(string) (string, error)
}
// RealPinentryRunner is the concrete implementation of PinentryRunner
type RealPinentryRunner struct {
PinentryBin string
}
// PinentryPrompter is a concrete implementation of the Prompter interface.
// It uses the default Cli under the hood, except for RequestSecurityCode, where
// it uses any _pinentry_ binary to capture the security code.
// Its purpose is mainly to capture the TOTP code outside of the TTY, and thus
// making it possible to use TOTP with the credential process.
// https://github.com/Versent/saml2aws#using-saml2aws-as-credential-process
type PinentryPrompter struct {
Runner PinentryRunner
DefaultPrompter Prompter
}
// NewPinentryPrompter is a factory for PinentryPrompter
func NewPinentryPrompter(bin string) *PinentryPrompter {
return &PinentryPrompter{Runner: NewRealPinentryRunner(bin), DefaultPrompter: NewCli()}
}
// NewRealPinentryRunner is a factory for RealPinentryRunner
func NewRealPinentryRunner(bin string) *RealPinentryRunner {
return &RealPinentryRunner{PinentryBin: bin}
}
// RequestSecurityCode for PinentryPrompter is creating a query for pinentry
// and sends it to the pinentry bin.
func (p *PinentryPrompter) RequestSecurityCode(pattern string) (output string) {
commandTemplate := "SETPROMPT %s\nGETPIN\n"
prompt := fmt.Sprintf(defaultPinentryDialog, pattern)
command := fmt.Sprintf(commandTemplate, prompt)
if output, err := p.Runner.Run(command); err != nil {
return ""
} else {
return output
}
}
// ChooseWithDefault is running the default CLI ChooseWithDefault
func (p *PinentryPrompter) ChooseWithDefault(prompt string, def string, choices []string) (string, error) {
return p.DefaultPrompter.ChooseWithDefault(prompt, def, choices)
}
// Choose is running the default CLI Choose
func (p *PinentryPrompter) Choose(pr string, options []string) int {
return p.DefaultPrompter.Choose(pr, options)
}
// StringRequired is runniner the default Cli StringRequired
func (p *PinentryPrompter) StringRequired(pr string) string {
return p.DefaultPrompter.StringRequired(pr)
}
// String is runniner the default Cli String
func (p *PinentryPrompter) String(pr string, defaultValue string) string {
return p.DefaultPrompter.String(pr, defaultValue)
}
// Password is runniner the default Cli Password
func (p *PinentryPrompter) Password(pr string) string {
return p.DefaultPrompter.Password(pr)
}
// Display is runniner the default Cli Display
func (p *PinentryPrompter) Display(pr string) {
p.DefaultPrompter.Display(pr)
}
// Run wraps a pinentry run. It sends the query to pinentry via stdin and
// reads its stdout to determine the user PIN.
// Pinentry uses an Assuan protocol
func (r *RealPinentryRunner) Run(command string) (output string, err error) {
cmd := exec.Command(r.PinentryBin, "--ttyname", "/dev/tty")
cmd.Stdin = strings.NewReader(command)
out, _ := cmd.StdoutPipe()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
err = cmd.Run()
// fmt.Println(err)
wg.Done()
}()
output, err = ParseResults(out)
wg.Wait()
return output, err
}
// ParseResults parses the standard output of the pinentry command and determine the
// user input, or wheter the program yielded any error
func ParseResults(pinEntryOutput io.Reader) (output string, err error) {
scanner := bufio.NewScanner(pinEntryOutput)
for scanner.Scan() {
line := scanner.Text()
// fmt.Println(line)
if strings.HasPrefix(line, "D ") {
output = line[2:]
}
if strings.HasPrefix(line, "ERR ") {
return "", fmt.Errorf("Error while running pinentry: %s", line[4:])
}
}
return output, err
}