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