-
Notifications
You must be signed in to change notification settings - Fork 567
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #744 from sledigabel/add_pinentry_prompter
Adding Pinentry as a prompter
- Loading branch information
Showing
6 changed files
with
353 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
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) | ||
} | ||
|
||
// 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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
package prompter | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
// creates a fake runner so we can perform unit tests | ||
type FakePinentryRunner struct { | ||
HasRun bool | ||
FakeOutput string | ||
FakePinentryOutput string | ||
PassedInput string | ||
} | ||
|
||
func (f *FakePinentryRunner) Run(cmd string) (string, error) { | ||
f.PassedInput = cmd | ||
f.HasRun = true | ||
if f.FakeOutput != "" { | ||
return f.FakeOutput, nil | ||
} | ||
if f.FakePinentryOutput != "" { | ||
reader := strings.NewReader(f.FakePinentryOutput) | ||
return ParseResults(reader) | ||
} | ||
return f.FakeOutput, nil | ||
} | ||
|
||
// FakeDefaultPrompter is a Mock prompter | ||
type FakeDefaultPrompter struct { | ||
CalledRequestSecurityCode bool | ||
CalledChoose bool | ||
CalledChooseWithDefault bool | ||
CalledString bool | ||
CalledStringRequired bool | ||
CalledPassword bool | ||
} | ||
|
||
// all the functions to implement the Prompter interface | ||
func (f *FakeDefaultPrompter) RequestSecurityCode(p string) string { | ||
f.CalledRequestSecurityCode = true | ||
return "" | ||
} | ||
func (f *FakeDefaultPrompter) Choose(p string, option []string) int { | ||
f.CalledChoose = true | ||
return 0 | ||
} | ||
func (f *FakeDefaultPrompter) ChooseWithDefault(p string, d string, c []string) (string, error) { | ||
f.CalledChooseWithDefault = true | ||
return "", nil | ||
} | ||
func (f *FakeDefaultPrompter) String(p string, defaultValue string) string { | ||
f.CalledString = true | ||
return "" | ||
} | ||
func (f *FakeDefaultPrompter) StringRequired(p string) string { | ||
f.CalledStringRequired = true | ||
return "" | ||
} | ||
func (f *FakeDefaultPrompter) Password(p string) string { | ||
f.CalledPassword = true | ||
return "" | ||
} | ||
|
||
func TestValidateAndSetPrompterShouldFailWithWrongInput(t *testing.T) { | ||
|
||
// backing up the current prompters for the other tests | ||
oldPrompter := ActivePrompter | ||
defer func() { ActivePrompter = oldPrompter }() | ||
|
||
errPrompts := []string{"abcde", "invalid", "prompt", " ", "pinentryfake"} | ||
for _, errPrompt := range errPrompts { | ||
err := ValidateAndSetPrompter(errPrompt) | ||
assert.Error(t, err) | ||
} | ||
|
||
} | ||
func TestValidateAndSetPrompterShouldWorkWithInputForCli(t *testing.T) { | ||
|
||
// backing up the current prompters for the other tests | ||
oldPrompter := ActivePrompter | ||
defer func() { ActivePrompter = oldPrompter }() | ||
|
||
errPrompts := []string{"", "default", "survey"} | ||
for _, errPrompt := range errPrompts { | ||
err := ValidateAndSetPrompter(errPrompt) | ||
assert.NoError(t, err) | ||
assert.IsType(t, ActivePrompter, NewCli()) | ||
} | ||
|
||
} | ||
func TestValidateAndSetPrompterShouldWorkWithInputForPinentry(t *testing.T) { | ||
|
||
// backing up the current prompters for the other tests | ||
oldPrompter := ActivePrompter | ||
defer func() { ActivePrompter = oldPrompter }() | ||
|
||
errPrompts := []string{"pinentry", "pinentry-tty", "pinentry-mac", "pinentry-gnome3"} | ||
for _, errPrompt := range errPrompts { | ||
err := ValidateAndSetPrompter(errPrompt) | ||
assert.NoError(t, err) | ||
assert.IsType(t, ActivePrompter, &PinentryPrompter{}) | ||
} | ||
|
||
} | ||
|
||
func TestChecksPinentryPrompterDefault(t *testing.T) { | ||
p := &PinentryPrompter{} | ||
fakeDefaultPrompter := &FakeDefaultPrompter{} | ||
p.DefaultPrompter = fakeDefaultPrompter | ||
|
||
_ = p.Choose("random", []string{"1", "2"}) | ||
assert.True(t, fakeDefaultPrompter.CalledChoose) | ||
|
||
_, _ = p.ChooseWithDefault("random", "random", []string{"1", "2"}) | ||
assert.True(t, fakeDefaultPrompter.CalledChooseWithDefault) | ||
|
||
_ = p.String("random", "random") | ||
assert.True(t, fakeDefaultPrompter.CalledString) | ||
|
||
_ = p.StringRequired("random") | ||
assert.True(t, fakeDefaultPrompter.CalledStringRequired) | ||
|
||
_ = p.Password("random") | ||
assert.True(t, fakeDefaultPrompter.CalledPassword) | ||
} | ||
|
||
func TestChecksPinentryPrompterCallsPinentryForRequestSecurityCode(t *testing.T) { | ||
p := &PinentryPrompter{} | ||
runner := &FakePinentryRunner{} | ||
p.Runner = runner | ||
runner.FakeOutput = "random_code" | ||
pinentryOutput := p.RequestSecurityCode("000000") | ||
|
||
assert.True(t, runner.HasRun) | ||
assert.Equal(t, pinentryOutput, "random_code") | ||
assert.Equal(t, runner.PassedInput, "SETPROMPT Security token [000000]\nGETPIN\n") | ||
|
||
} | ||
|
||
func TestChecksPinentryPrompterReturnsRightCodeGivenFakePinentryOutput(t *testing.T) { | ||
p := &PinentryPrompter{} | ||
runner := &FakePinentryRunner{} | ||
p.Runner = runner | ||
runner.FakePinentryOutput = "OK This line should get ignored\nOK This line should too\nD 654321\nOK Final\n" | ||
pinentryOutput := p.RequestSecurityCode("000000") | ||
|
||
assert.True(t, runner.HasRun) | ||
assert.Equal(t, pinentryOutput, "654321") | ||
|
||
} | ||
|
||
func TestChecksPinentryPrompterReturnsNoCodeGivenErroneousFakePinentryOutput(t *testing.T) { | ||
p := &PinentryPrompter{} | ||
runner := &FakePinentryRunner{} | ||
p.Runner = runner | ||
runner.FakePinentryOutput = "OK This line should get ignored\nOK This line should too\nERR This is an error\nD 654321\nOK Final\n" | ||
pinentryOutput := p.RequestSecurityCode("000000") | ||
|
||
assert.True(t, runner.HasRun) | ||
assert.Equal(t, pinentryOutput, "") | ||
} | ||
|
||
func TestParseOutputShouldThrowError(t *testing.T) { | ||
|
||
input := strings.NewReader("OK Ignore this line\nERR This is an error\nD This result should be ignored\nOK this is irrelevant\n") | ||
output, err := ParseResults(input) | ||
|
||
assert.Error(t, err) | ||
assert.Equal(t, output, "") | ||
} | ||
|
||
func TestParseOutputShouldReturnCorrectOutput(t *testing.T) { | ||
|
||
input := strings.NewReader("OK Ignore this line\nD THISISTHERETURN\nOK this is irrelevant\n") | ||
output, err := ParseResults(input) | ||
|
||
assert.NoError(t, err) | ||
assert.Equal(t, output, "THISISTHERETURN") | ||
} |
Oops, something went wrong.