// Package passphrase is a utility function for managing passphrase // for TUF and Notary keys. package passphrase import ( "bufio" "errors" "fmt" "io" "os" "strings" "path/filepath" "github.com/docker/docker/pkg/term" ) // Retriever is a callback function that should retrieve a passphrase // for a given named key. If it should be treated as new passphrase (e.g. with // confirmation), createNew will be true. Attempts is passed in so that implementers // decide how many chances to give to a human, for example. type Retriever func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) const ( idBytesToDisplay = 7 tufRootAlias = "root" tufTargetsAlias = "targets" tufSnapshotAlias = "snapshot" tufRootKeyGenerationWarning = `You are about to create a new root signing key passphrase. This passphrase will be used to protect the most sensitive key in your signing system. Please choose a long, complex passphrase and be careful to keep the password and the key file itself secure and backed up. It is highly recommended that you use a password manager to generate the passphrase and keep it safe. There will be no way to recover this key. You can find the key in your config directory.` ) var ( // ErrTooShort is returned if the passphrase entered for a new key is // below the minimum length ErrTooShort = errors.New("Passphrase too short") // ErrDontMatch is returned if the two entered passphrases don't match. // new key is below the minimum length ErrDontMatch = errors.New("The entered passphrases do not match") // ErrTooManyAttempts is returned if the maximum number of passphrase // entry attempts is reached. ErrTooManyAttempts = errors.New("Too many attempts") ) // PromptRetriever returns a new Retriever which will provide a prompt on stdin // and stdout to retrieve a passphrase. The passphrase will be cached such that // subsequent prompts will produce the same passphrase. func PromptRetriever() Retriever { return PromptRetrieverWithInOut(os.Stdin, os.Stdout, nil) } // PromptRetrieverWithInOut returns a new Retriever which will provide a // prompt using the given in and out readers. The passphrase will be cached // such that subsequent prompts will produce the same passphrase. // aliasMap can be used to specify display names for TUF key aliases. If aliasMap // is nil, a sensible default will be used. func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]string) Retriever { userEnteredTargetsSnapshotsPass := false targetsSnapshotsPass := "" userEnteredRootsPass := false rootsPass := "" return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { if alias == tufRootAlias && createNew && numAttempts == 0 { fmt.Fprintln(out, tufRootKeyGenerationWarning) } if numAttempts > 0 { if !createNew { fmt.Fprintln(out, "Passphrase incorrect. Please retry.") } } // Figure out if we should display a different string for this alias displayAlias := alias if aliasMap != nil { if val, ok := aliasMap[alias]; ok { displayAlias = val } } // First, check if we have a password cached for this alias. if numAttempts == 0 { if userEnteredTargetsSnapshotsPass && (alias == tufSnapshotAlias || alias == tufTargetsAlias) { return targetsSnapshotsPass, false, nil } if userEnteredRootsPass && (alias == "root") { return rootsPass, false, nil } } if numAttempts > 3 && !createNew { return "", true, ErrTooManyAttempts } // If typing on the terminal, we do not want the terminal to echo the // password that is typed (so it doesn't display) if term.IsTerminal(0) { state, err := term.SaveState(0) if err != nil { return "", false, err } term.DisableEcho(0, state) defer term.RestoreTerminal(0, state) } stdin := bufio.NewReader(in) indexOfLastSeparator := strings.LastIndex(keyName, string(filepath.Separator)) if indexOfLastSeparator == -1 { indexOfLastSeparator = 0 } var shortName string if len(keyName) > indexOfLastSeparator+idBytesToDisplay { if indexOfLastSeparator > 0 { keyNamePrefix := keyName[:indexOfLastSeparator] keyNameID := keyName[indexOfLastSeparator+1 : indexOfLastSeparator+idBytesToDisplay+1] shortName = keyNameID + " (" + keyNamePrefix + ")" } else { shortName = keyName[indexOfLastSeparator : indexOfLastSeparator+idBytesToDisplay] } } withID := fmt.Sprintf(" with ID %s", shortName) if shortName == "" { withID = "" } if createNew { fmt.Fprintf(out, "Enter passphrase for new %s key%s: ", displayAlias, withID) } else if displayAlias == "yubikey" { fmt.Fprintf(out, "Enter the %s for the attached Yubikey: ", keyName) } else { fmt.Fprintf(out, "Enter passphrase for %s key%s: ", displayAlias, withID) } passphrase, err := stdin.ReadBytes('\n') fmt.Fprintln(out) if err != nil { return "", false, err } retPass := strings.TrimSpace(string(passphrase)) if !createNew { if alias == tufSnapshotAlias || alias == tufTargetsAlias { userEnteredTargetsSnapshotsPass = true targetsSnapshotsPass = retPass } if alias == tufRootAlias { userEnteredRootsPass = true rootsPass = retPass } return retPass, false, nil } if len(retPass) < 8 { fmt.Fprintln(out, "Passphrase is too short. Please use a password manager to generate and store a good random passphrase.") return "", false, ErrTooShort } fmt.Fprintf(out, "Repeat passphrase for new %s key%s: ", displayAlias, withID) confirmation, err := stdin.ReadBytes('\n') fmt.Fprintln(out) if err != nil { return "", false, err } confirmationStr := strings.TrimSpace(string(confirmation)) if retPass != confirmationStr { fmt.Fprintln(out, "Passphrases do not match. Please retry.") return "", false, ErrDontMatch } if alias == tufSnapshotAlias || alias == tufTargetsAlias { userEnteredTargetsSnapshotsPass = true targetsSnapshotsPass = retPass } if alias == tufRootAlias { userEnteredRootsPass = true rootsPass = retPass } return retPass, false, nil } } // ConstantRetriever returns a new Retriever which will return a constant string // as a passphrase. func ConstantRetriever(constantPassphrase string) Retriever { return func(k, a string, c bool, n int) (string, bool, error) { return constantPassphrase, false, nil } }