1// Package passphrase is a utility function for managing passphrase
2// for TUF and Notary keys.
3package passphrase
4
5import (
6	"bufio"
7	"errors"
8	"fmt"
9	"io"
10	"os"
11	"path/filepath"
12	"strings"
13
14	"github.com/theupdateframework/notary"
15	"golang.org/x/crypto/ssh/terminal"
16)
17
18const (
19	idBytesToDisplay            = 7
20	tufRootAlias                = "root"
21	tufRootKeyGenerationWarning = `You are about to create a new root signing key passphrase. This passphrase
22will be used to protect the most sensitive key in your signing system. Please
23choose a long, complex passphrase and be careful to keep the password and the
24key file itself secure and backed up. It is highly recommended that you use a
25password manager to generate the passphrase and keep it safe. There will be no
26way to recover this key. You can find the key in your config directory.`
27)
28
29var (
30	// ErrTooShort is returned if the passphrase entered for a new key is
31	// below the minimum length
32	ErrTooShort = errors.New("Passphrase too short")
33
34	// ErrDontMatch is returned if the two entered passphrases don't match.
35	// new key is below the minimum length
36	ErrDontMatch = errors.New("The entered passphrases do not match")
37
38	// ErrTooManyAttempts is returned if the maximum number of passphrase
39	// entry attempts is reached.
40	ErrTooManyAttempts = errors.New("Too many attempts")
41
42	// ErrNoInput is returned if we do not have a valid input method for passphrases
43	ErrNoInput = errors.New("Please either use environment variables or STDIN with a terminal to provide key passphrases")
44)
45
46// PromptRetriever returns a new Retriever which will provide a prompt on stdin
47// and stdout to retrieve a passphrase. stdin will be checked if it is a terminal,
48// else the PromptRetriever will error when attempting to retrieve a passphrase.
49// Upon successful passphrase retrievals, the passphrase will be cached such that
50// subsequent prompts will produce the same passphrase.
51func PromptRetriever() notary.PassRetriever {
52	if !terminal.IsTerminal(int(os.Stdin.Fd())) {
53		return func(string, string, bool, int) (string, bool, error) {
54			return "", false, ErrNoInput
55		}
56	}
57	return PromptRetrieverWithInOut(os.Stdin, os.Stdout, nil)
58}
59
60type boundRetriever struct {
61	in              io.Reader
62	out             io.Writer
63	aliasMap        map[string]string
64	passphraseCache map[string]string
65}
66
67func (br *boundRetriever) getPassphrase(keyName, alias string, createNew bool, numAttempts int) (string, bool, error) {
68	if numAttempts == 0 {
69		if alias == tufRootAlias && createNew {
70			fmt.Fprintln(br.out, tufRootKeyGenerationWarning)
71		}
72
73		if pass, ok := br.passphraseCache[alias]; ok {
74			return pass, false, nil
75		}
76	} else if !createNew { // per `if`, numAttempts > 0 if we're at this `else`
77		if numAttempts > 3 {
78			return "", true, ErrTooManyAttempts
79		}
80		fmt.Fprintln(br.out, "Passphrase incorrect. Please retry.")
81	}
82
83	// passphrase not cached and we're not aborting, get passphrase from user!
84	return br.requestPassphrase(keyName, alias, createNew, numAttempts)
85}
86
87func (br *boundRetriever) requestPassphrase(keyName, alias string, createNew bool, numAttempts int) (string, bool, error) {
88	// Figure out if we should display a different string for this alias
89	displayAlias := alias
90	if val, ok := br.aliasMap[alias]; ok {
91		displayAlias = val
92	}
93
94	indexOfLastSeparator := strings.LastIndex(keyName, string(filepath.Separator))
95	if indexOfLastSeparator == -1 {
96		indexOfLastSeparator = 0
97	}
98
99	var shortName string
100	if len(keyName) > indexOfLastSeparator+idBytesToDisplay {
101		if indexOfLastSeparator > 0 {
102			keyNamePrefix := keyName[:indexOfLastSeparator]
103			keyNameID := keyName[indexOfLastSeparator+1 : indexOfLastSeparator+idBytesToDisplay+1]
104			shortName = keyNameID + " (" + keyNamePrefix + ")"
105		} else {
106			shortName = keyName[indexOfLastSeparator : indexOfLastSeparator+idBytesToDisplay]
107		}
108	}
109
110	withID := fmt.Sprintf(" with ID %s", shortName)
111	if shortName == "" {
112		withID = ""
113	}
114
115	switch {
116	case createNew:
117		fmt.Fprintf(br.out, "Enter passphrase for new %s key%s: ", displayAlias, withID)
118	case displayAlias == "yubikey":
119		fmt.Fprintf(br.out, "Enter the %s for the attached Yubikey: ", keyName)
120	default:
121		fmt.Fprintf(br.out, "Enter passphrase for %s key%s: ", displayAlias, withID)
122	}
123
124	stdin := bufio.NewReader(br.in)
125	passphrase, err := GetPassphrase(stdin)
126	fmt.Fprintln(br.out)
127	if err != nil {
128		return "", false, err
129	}
130
131	retPass := strings.TrimSpace(string(passphrase))
132
133	if createNew {
134		err = br.verifyAndConfirmPassword(stdin, retPass, displayAlias, withID)
135		if err != nil {
136			return "", false, err
137		}
138	}
139
140	br.cachePassword(alias, retPass)
141
142	return retPass, false, nil
143}
144
145func (br *boundRetriever) verifyAndConfirmPassword(stdin *bufio.Reader, retPass, displayAlias, withID string) error {
146	if len(retPass) < 8 {
147		fmt.Fprintln(br.out, "Passphrase is too short. Please use a password manager to generate and store a good random passphrase.")
148		return ErrTooShort
149	}
150
151	fmt.Fprintf(br.out, "Repeat passphrase for new %s key%s: ", displayAlias, withID)
152
153	confirmation, err := GetPassphrase(stdin)
154	fmt.Fprintln(br.out)
155	if err != nil {
156		return err
157	}
158	confirmationStr := strings.TrimSpace(string(confirmation))
159
160	if retPass != confirmationStr {
161		fmt.Fprintln(br.out, "Passphrases do not match. Please retry.")
162		return ErrDontMatch
163	}
164	return nil
165}
166
167func (br *boundRetriever) cachePassword(alias, retPass string) {
168	br.passphraseCache[alias] = retPass
169}
170
171// PromptRetrieverWithInOut returns a new Retriever which will provide a
172// prompt using the given in and out readers. The passphrase will be cached
173// such that subsequent prompts will produce the same passphrase.
174// aliasMap can be used to specify display names for TUF key aliases. If aliasMap
175// is nil, a sensible default will be used.
176func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]string) notary.PassRetriever {
177	bound := &boundRetriever{
178		in:              in,
179		out:             out,
180		aliasMap:        aliasMap,
181		passphraseCache: make(map[string]string),
182	}
183
184	return bound.getPassphrase
185}
186
187// ConstantRetriever returns a new Retriever which will return a constant string
188// as a passphrase.
189func ConstantRetriever(constantPassphrase string) notary.PassRetriever {
190	return func(k, a string, c bool, n int) (string, bool, error) {
191		return constantPassphrase, false, nil
192	}
193}
194
195// GetPassphrase get the passphrase from bufio.Reader or from terminal.
196// If typing on the terminal, we disable terminal to echo the passphrase.
197func GetPassphrase(in *bufio.Reader) ([]byte, error) {
198	var (
199		passphrase []byte
200		err        error
201	)
202
203	if terminal.IsTerminal(int(os.Stdin.Fd())) {
204		passphrase, err = terminal.ReadPassword(int(os.Stdin.Fd()))
205	} else {
206		passphrase, err = in.ReadBytes('\n')
207	}
208
209	return passphrase, err
210}
211