1package libkb
2
3import (
4	"errors"
5	"fmt"
6	"strings"
7	"time"
8
9	keybase1 "github.com/keybase/client/go/protocol/keybase1"
10)
11
12func GetKeybasePassphrase(m MetaContext, ui SecretUI, arg keybase1.GUIEntryArg) (keybase1.GetPassphraseRes, error) {
13	resCh := make(chan keybase1.GetPassphraseRes)
14	errCh := make(chan error)
15	go func() {
16		res, err := GetPassphraseUntilCheckWithChecker(m, arg,
17			newUIPrompter(ui), &CheckPassphraseSimple)
18		if err != nil {
19			errCh <- err
20			return
21		}
22		res.StoreSecret = true
23		resCh <- res
24	}()
25
26	select {
27	case res := <-resCh:
28		return res, nil
29	case err := <-errCh:
30		return keybase1.GetPassphraseRes{}, err
31	case <-time.After(3 * time.Minute):
32		return keybase1.GetPassphraseRes{}, TimeoutError{}
33	}
34}
35
36func GetSecret(m MetaContext, ui SecretUI, title, prompt, retryMsg string, allowSecretStore bool) (keybase1.GetPassphraseRes, error) {
37	arg := DefaultPassphraseArg(m)
38	arg.WindowTitle = title
39	arg.Type = keybase1.PassphraseType_PASS_PHRASE
40	arg.Prompt = prompt
41	arg.RetryLabel = retryMsg
42	res, err := GetPassphraseUntilCheckWithChecker(m, arg, newUIPrompter(ui), &CheckPassphraseSimple)
43	if err != nil {
44		return res, err
45	}
46	res.StoreSecret = allowSecretStore
47	return res, nil
48}
49
50func GetPaperKeyPassphrase(m MetaContext, ui SecretUI, username string, lastErr error, expectedPrefix *string) (string, error) {
51	arg := DefaultPassphraseArg(m)
52	arg.WindowTitle = "Paper Key"
53	arg.Type = keybase1.PassphraseType_PAPER_KEY
54	if len(username) == 0 {
55		username = "your account"
56	}
57	arg.Prompt = fmt.Sprintf("Please enter a paper key for %s", username)
58	arg.Username = username
59	arg.Features.ShowTyping.Allow = true
60	arg.Features.ShowTyping.DefaultValue = true
61	if lastErr != nil {
62		arg.RetryLabel = lastErr.Error()
63	}
64	res, err := GetPassphraseUntilCheck(m, arg, newUIPrompter(ui), &PaperChecker{expectedPrefix})
65	if err != nil {
66		return "", err
67	}
68	return res.Passphrase, nil
69}
70
71func GetPaperKeyForCryptoPassphrase(m MetaContext, ui SecretUI, reason string, devices []*Device) (string, error) {
72	if len(devices) == 0 {
73		return "", errors.New("empty device list")
74	}
75	arg := DefaultPassphraseArg(m)
76	arg.WindowTitle = "Paper Key"
77	arg.Type = keybase1.PassphraseType_PAPER_KEY
78	arg.Features.ShowTyping.Allow = true
79	arg.Features.ShowTyping.DefaultValue = true
80	if len(devices) == 1 {
81		arg.Prompt = fmt.Sprintf("%s: please enter the paper key '%s...'", reason, *devices[0].Description)
82	} else {
83		descs := make([]string, len(devices))
84		for i, dev := range devices {
85			descs[i] = fmt.Sprintf("'%s...'", *dev.Description)
86		}
87		paperOpts := strings.Join(descs, " or ")
88		arg.Prompt = fmt.Sprintf("%s: please enter one of the following paper keys %s", reason, paperOpts)
89	}
90
91	res, err := GetPassphraseUntilCheck(m, arg, newUIPrompter(ui), &PaperChecker{})
92	if err != nil {
93		return "", err
94	}
95	return res.Passphrase, nil
96}
97
98func GetNewKeybasePassphrase(mctx MetaContext, ui SecretUI, arg keybase1.GUIEntryArg, confirm string) (keybase1.GetPassphraseRes, error) {
99	initialPrompt := arg.Prompt
100
101	for i := 0; i < 10; i++ {
102		res, err := GetPassphraseUntilCheckWithChecker(mctx, arg,
103			newUIPrompter(ui), &CheckPassphraseNew)
104		if err != nil {
105			return keybase1.GetPassphraseRes{}, nil
106		}
107
108		// confirm the password
109		arg.RetryLabel = ""
110		arg.Prompt = confirm
111		confirm, err := GetPassphraseUntilCheckWithChecker(mctx, arg,
112			newUIPrompter(ui), &CheckPassphraseNew)
113		if err != nil {
114			return keybase1.GetPassphraseRes{}, nil
115		}
116
117		if res.Passphrase == confirm.Passphrase {
118			return res, nil
119		}
120
121		// setup the prompt, label for new first attempt
122		arg.Prompt = initialPrompt
123		arg.RetryLabel = "Passphrase mismatch"
124	}
125
126	return keybase1.GetPassphraseRes{}, RetryExhaustedError{}
127}
128
129type PassphrasePrompter interface {
130	Prompt(keybase1.GUIEntryArg) (keybase1.GetPassphraseRes, error)
131}
132
133type uiPrompter struct {
134	ui SecretUI
135}
136
137var _ PassphrasePrompter = &uiPrompter{}
138
139func newUIPrompter(ui SecretUI) *uiPrompter {
140	return &uiPrompter{ui: ui}
141}
142
143func (u *uiPrompter) Prompt(arg keybase1.GUIEntryArg) (keybase1.GetPassphraseRes, error) {
144	return u.ui.GetPassphrase(arg, nil)
145}
146
147func GetPassphraseUntilCheckWithChecker(m MetaContext, arg keybase1.GUIEntryArg, prompter PassphrasePrompter, checker *Checker) (keybase1.GetPassphraseRes, error) {
148	if checker == nil {
149		return keybase1.GetPassphraseRes{}, errors.New("nil passphrase checker")
150	}
151	w := &CheckerWrapper{checker: *checker}
152	return GetPassphraseUntilCheck(m, arg, prompter, w)
153}
154
155func GetPassphraseUntilCheck(m MetaContext, arg keybase1.GUIEntryArg, prompter PassphrasePrompter, checker PassphraseChecker) (keybase1.GetPassphraseRes, error) {
156	for i := 0; i < 10; i++ {
157		res, err := prompter.Prompt(arg)
158		if err != nil {
159			return keybase1.GetPassphraseRes{}, err
160		}
161		if checker == nil {
162			return res, nil
163		}
164
165		s := res.Passphrase
166		t, err := checker.Automutate(m, s)
167		if err != nil {
168			return keybase1.GetPassphraseRes{}, err
169		}
170		res = keybase1.GetPassphraseRes{Passphrase: t, StoreSecret: res.StoreSecret}
171
172		err = checker.Check(m, res.Passphrase)
173		if err == nil {
174			return res, nil
175		}
176		arg.RetryLabel = err.Error()
177	}
178	return keybase1.GetPassphraseRes{}, RetryExhaustedError{}
179}
180
181func DefaultPassphraseArg(m MetaContext) keybase1.GUIEntryArg {
182	arg := keybase1.GUIEntryArg{
183		SubmitLabel: "Submit",
184		CancelLabel: "Cancel",
185		Features: keybase1.GUIEntryFeatures{
186			ShowTyping: keybase1.Feature{
187				Allow:        true,
188				DefaultValue: false,
189				Readonly:     true,
190				Label:        "Show typing",
191			},
192		},
193	}
194	return arg
195}
196
197func DefaultPassphrasePromptArg(mctx MetaContext, username string) keybase1.GUIEntryArg {
198	arg := DefaultPassphraseArg(mctx)
199	arg.WindowTitle = "Keybase password"
200	arg.Type = keybase1.PassphraseType_PASS_PHRASE
201	arg.Username = username
202	arg.Prompt = fmt.Sprintf("Please enter the Keybase password for %s (%d+ characters)", username, MinPassphraseLength)
203	return arg
204}
205
206// PassphraseChecker is an interface for checking the format of a
207// passphrase. Returns nil if the format is ok, or a descriptive
208// hint otherwise.
209type PassphraseChecker interface {
210	Check(MetaContext, string) error
211	Automutate(MetaContext, string) (string, error)
212}
213
214// CheckerWrapper wraps a Checker type to make it conform to the
215// PassphraseChecker interface.
216type CheckerWrapper struct {
217	checker Checker
218}
219
220func (w *CheckerWrapper) Automutate(m MetaContext, s string) (string, error) {
221	return s, nil
222}
223
224// Check s using checker, respond with checker.Hint if check
225// fails.
226func (w *CheckerWrapper) Check(m MetaContext, s string) error {
227	if w.checker.F(s) {
228		return nil
229	}
230	return errors.New(w.checker.Hint)
231}
232
233// PaperChecker implements PassphraseChecker for paper keys.
234type PaperChecker struct {
235	expectedPrefix *string
236}
237
238func (p *PaperChecker) Automutate(m MetaContext, s string) (string, error) {
239	phrase := NewPaperKeyPhrase(s)
240	if phrase.NumWords() == PaperKeyNoPrefixLen {
241		if p.expectedPrefix == nil {
242			return "", errors.New("No prefix given but expectedPrefix is nil; must give the entire paper key.")
243		}
244		return fmt.Sprintf("%s %s", *p.expectedPrefix, s), nil
245	}
246	return s, nil
247}
248
249// Check a paper key format.  Will return a detailed error message
250// specific to the problems found in s.
251func (p *PaperChecker) Check(m MetaContext, s string) error {
252	phrase := NewPaperKeyPhrase(s)
253
254	// check for empty
255	if len(phrase.String()) == 0 {
256		m.Debug("paper phrase is empty")
257		return NewPaperKeyError("paper key was empty", true)
258	}
259
260	// check for at least PaperKeyWordCountMin words
261	if phrase.NumWords() < PaperKeyWordCountMin {
262		return NewPaperKeyError(fmt.Sprintf("your paper key should have at least %d words", PaperKeyWordCountMin), true)
263	}
264
265	// check for invalid words
266	invalids := phrase.InvalidWords()
267	if len(invalids) > 0 {
268		m.Debug("paper phrase has invalid word(s) in it")
269		var err error
270		var w []string
271		for _, i := range invalids {
272			w = append(w, fmt.Sprintf("%q", i))
273		}
274		if len(invalids) > 1 {
275			err = NewPaperKeyError(fmt.Sprintf("the words %s are invalid", strings.Join(w, ", ")), true)
276		} else {
277			err = NewPaperKeyError(fmt.Sprintf("the word %s is invalid", w[0]), true)
278		}
279		return err
280	}
281
282	// check version
283	version, err := phrase.Version()
284	if err != nil {
285		m.Debug("error getting paper key version: %s", err)
286		// despite the error, just tell the user the paper key is wrong:
287		return NewPaperKeyError("key didn't match any known keys for this account", true)
288	}
289	if version != PaperKeyVersion {
290		m.Debug("paper key version mismatch: generated version = %d, libkb version = %d", version, PaperKeyVersion)
291		return NewPaperKeyError("key didn't match any known keys for this account", true)
292	}
293
294	return nil
295}
296