diff options
| author | Joe Richey joerichey@google.com <joerichey@google.com> | 2017-06-21 10:21:21 -0700 |
|---|---|---|
| committer | Joe Richey joerichey@google.com <joerichey@google.com> | 2017-06-28 15:15:21 -0700 |
| commit | 37c866e1e16a6d2dded11ba93c2e04af3764a139 (patch) | |
| tree | 745d548ed30e9e70b4702622510690af62a48b58 /cmd/fscrypt/prompt.go | |
| parent | 93415b198a3ef427c02893b8fdf036aa75ffe50f (diff) | |
cmd/fscrypt: setup, encrypt, unlock commands
This commit adds in the framework for adding commands and subcommands to
the fscrypt tool. This commit adds in the "setup", "encrypt", and
"unlock" commands. Additional information can be found by running:
fscrypt <command> --help.
This commit defines how flags are parsed and errors are handled. It also
creates an extensible framework for prompting the user for information.
Change-Id: I159d7f44ee2b2bbc5e072f0802850e082d9a13ce
Diffstat (limited to 'cmd/fscrypt/prompt.go')
| -rw-r--r-- | cmd/fscrypt/prompt.go | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/cmd/fscrypt/prompt.go b/cmd/fscrypt/prompt.go new file mode 100644 index 0000000..56dcf06 --- /dev/null +++ b/cmd/fscrypt/prompt.go @@ -0,0 +1,322 @@ +/* + * prompt.go - Functions for handling user input and options + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "os/user" + "strconv" + "strings" + + "fscrypt/actions" + "fscrypt/metadata" +) + +const ( + // Suffixes for questions with a yes or no default + defaultYesSuffix = " [Y/n] " + defaultNoSuffix = " [y/N] " +) + +// Descriptions for each of the protector sources +var sourceDescriptions = map[metadata.SourceType]string{ + metadata.SourceType_pam_passphrase: "Your login passphrase", + metadata.SourceType_custom_passphrase: "A custom passphrase", + metadata.SourceType_raw_key: "A raw 256-bit key", +} + +// promptUser presents a message to the user and returns their input string. An +// error is returned if our read from standard input fails. +func promptUser(prompt string) (string, error) { + scanner := bufio.NewScanner(os.Stdin) + fmt.Print(prompt) + if !scanner.Scan() { + return "", ErrReadingStdin + } + return scanner.Text(), nil +} + +// askQuestion asks the user a yes or no question. Returning a boolean on a +// successful answer and an error if there was not a response from the user. +// Returns the defaultChoice on empty input (or in quiet mode). +func askQuestion(question string, defaultChoice bool) (bool, error) { + // If in quiet mode, we just use the default + if quietFlag.Value { + return defaultChoice, nil + } + // Loop until failure or valid input + var input string + var err error + for { + if defaultChoice { + input, err = promptUser(question + defaultYesSuffix) + } else { + input, err = promptUser(question + defaultNoSuffix) + } + if err != nil { + return false, err + } + + switch strings.ToLower(input) { + case "y", "yes": + return true, nil + case "n", "no": + return false, nil + case "": + return defaultChoice, nil + } + } +} + +// askConfirmation asks the user for confirmation of a specific action. An error +// is returned if the user declines or IO fails. +func askConfirmation(question string, defaultChoice bool, warning string) error { + // All confirmations are "yes" if we are forcing. + if forceFlag.Value { + return nil + } + + // Defaults of "no" require forcing. + if !defaultChoice { + if quietFlag.Value { + return ErrNoDesctructiveOps + } + } + + if warning != "" && !quietFlag.Value { + fmt.Println(wrapText("WARNING: "+warning, 0)) + } + + confirmed, err := askQuestion(question, defaultChoice) + if err != nil { + return err + } + if !confirmed { + return ErrCanceled + } + return nil +} + +// getUsername returns the username for the provided UID. If the UID does not +// correspond to a user or the username is blank, "UID=<uid>" is returned. +func getUsername(uid int64) string { + u, err := user.LookupId(strconv.Itoa(int(uid))) + if err != nil || u.Username == "" { + return fmt.Sprintf("UID=%d", uid) + } + return u.Username +} + +// formatInfo gives a string description of metadata.ProtectorData. +func formatInfo(data actions.ProtectorInfo) string { + switch data.Source() { + case metadata.SourceType_pam_passphrase: + return "login protector for " + getUsername(data.UID()) + case metadata.SourceType_custom_passphrase: + return fmt.Sprintf("custom protector %q", data.Name()) + case metadata.SourceType_raw_key: + return fmt.Sprintf("raw key protector %q", data.Name()) + default: + panic(ErrInvalidSource) + } +} + +// promptForName gets a name from user input (or flags) and returns it. +func promptForName(ctx *actions.Context) (string, error) { + // A name flag means we do not need to prompt + if nameFlag.Value != "" { + return nameFlag.Value, nil + } + + // Don't ask for a name if we do not need it + if quietFlag.Value || ctx.Config.Source == metadata.SourceType_pam_passphrase { + return "", nil + } + + for { + name, err := promptUser("Enter a name for the new protector: ") + if err != nil { + return "", err + } + if name != "" { + return name, nil + } + } +} + +// promptForSource gets a source type from user input (or flags) and modifies +// the context to use that source. +func promptForSource(ctx *actions.Context) error { + // A source flag overrides everything else. + if sourceFlag.Value != "" { + val, ok := metadata.SourceType_value[sourceFlag.Value] + if !ok || val == 0 { + return ErrInvalidSource + } + ctx.Config.Source = metadata.SourceType(val) + return nil + } + + // Just use the default in quiet mode + if quietFlag.Value { + return nil + } + + // We print all the sources with their number, description, and name. + fmt.Println("Your data can be protected with one of the following sources:") + for idx := 1; idx < len(metadata.SourceType_value); idx++ { + source := metadata.SourceType(idx) + description := sourceDescriptions[source] + fmt.Printf("%d - %s (%s)\n", idx, description, source) + } + + prompt := fmt.Sprintf("Enter the source number for the new protector [%d - %s]: ", + ctx.Config.Source, ctx.Config.Source) + for { + input, err := promptUser(prompt) + if err != nil { + return err + } + + // Use the default if the user just hits enter + if input == "" { + return nil + } + + // Check for a valid index, reprompt if invalid. + index, err := strconv.Atoi(input) + if err == nil && index >= 1 && index < len(metadata.SourceType_value) { + ctx.Config.Source = metadata.SourceType(index) + return nil + } + } +} + +// promptForKeyFile returns an open file that should be used to create or unlock +// a raw_key protector. Be sure to close the file when done. +func promptForKeyFile(prompt string) (*os.File, error) { + // If specified on the command line, we only try no open it once. + if keyFileFlag.Value != "" { + return os.Open(keyFileFlag.Value) + } + if quietFlag.Value { + return nil, ErrSpecifyKeyFile + } + + // Prompt for a valid path until we get a file we can open. + for { + filename, err := promptUser(prompt) + if err != nil { + return nil, err + } + file, err := os.Open(filename) + if err == nil { + return file, nil + } + fmt.Println(err) + } + +} + +// promptForProtector, given a non-empty list of protector options, uses user +// input to select the desired protector. If there is only one option to choose +// from, that protector is automatically selected. +func promptForProtector(options []*actions.ProtectorOption) (int, error) { + numOptions := len(options) + log.Printf("selecting from %s", pluralize(numOptions, "protector")) + + // Get the number of load errors. + numLoadErrors := 0 + for _, option := range options { + if option.LoadError != nil { + log.Printf("when loading option: %v", option.LoadError) + numLoadErrors++ + } + } + + if numLoadErrors == numOptions { + return 0, ErrAllLoadsFailed + } + if numOptions == 1 { + return 0, nil + } + if quietFlag.Value { + return 0, ErrSpecifyProtector + } + + // List all of the protector options which did not have a load error. + fmt.Println("The available protectors are: ") + for idx, option := range options { + if option.LoadError != nil { + continue + } + + description := fmt.Sprintf("%d - %s", idx, formatInfo(option.ProtectorInfo)) + if option.LinkedMount != nil { + description += fmt.Sprintf(" (linked protector on %q)", option.LinkedMount.Path) + } + fmt.Println(description) + } + + if numLoadErrors > 0 { + fmt.Printf(wrapText("NOTE: %d of the %d protectors failed to load. "+loadHelpText, 0)) + } + + for { + input, err := promptUser("Enter the number of protector to use: ") + if err != nil { + return 0, err + } + + // Check for a valid index, reprompt if invalid. + index, err := strconv.Atoi(input) + if err == nil && index >= 0 && index < len(options) { + return index, nil + } + } +} + +// optionFn is an actions.OptionFunc which handles selecting an option for a +// specific policy. This is either done interactively, or by deferring to the +// protectorFlag. +func optionFn(policyDescriptor string, options []*actions.ProtectorOption) (int, error) { + // If we have an unlock-with flag, we directly select the specified + // protector to unlock the policy. + if unlockWithFlag.Value != "" { + log.Printf("optionFn(%s) w/ unlock flag", policyDescriptor) + protector, err := getProtectorFromFlag(unlockWithFlag.Value) + if err != nil { + return 0, err + } + + for idx, option := range options { + if option.Descriptor() == protector.Descriptor() { + return idx, nil + } + } + return 0, actions.ErrNotProtected + } + + log.Printf("optionFn(%s)", policyDescriptor) + return promptForProtector(options) +} |