diff --git a/.gitignore b/.gitignore index 345dfa19..c308c328 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ fscrypt -fscrypt.* +fscrypt-ext4 fscrypt_image pam_fscrypt.so .vscode diff --git a/Makefile b/Makefile index a28522e6..cbbaea76 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ RELEASE_VERSION = 0.2.0 NAME = fscrypt +EXT4_NAME = $(NAME)-ext4 PAM_NAME = pam_$(NAME) PAM_MODULE = $(PAM_NAME).so @@ -27,8 +28,10 @@ DESTDIR ?= /usr/local/bin PAM_MODULE_DIR ?= /lib/security PAM_CONFIG_DIR ?= /usr/share/pam-configs -CMD_PKG = github.com/google/$(NAME)/cmd/$(NAME) -PAM_PKG = github.com/google/$(NAME)/$(PAM_NAME) +PKG_DIR = github.com/google/$(NAME) +CMD_PKG = $(PKG_DIR)/cmd/$(NAME) +EXT4_PKG =$(PKG_DIR)/cmd/ext4 +PAM_PKG = $(PKG_DIR)/$(PAM_NAME) SRC_FILES = $(shell find . -type f -name '*.go' -o -name "*.h" -o -name "*.c") GO_FILES = $(shell find . -type f -name '*.go' -not -path "./vendor/*") @@ -84,10 +87,10 @@ GO_LINK_FLAGS ?= -s -w # Pass the version to the command line program (pulled from tags). TAG_VERSION = $(shell git describe --tags) VERSION = $(if $(TAG_VERSION),$(TAG_VERSION),$(RELEASE_VERSION)) -VERSION_FLAG = -X "main.version=$(VERSION)" +VERSION_FLAG = -X "$(PKG_DIR)/cmd.VersionTag=$(VERSION)" # Pass the current date and time to the command line program. -DATE_FLAG = -X "main.buildTime=$(shell date)" +DATE_FLAG = -X "$(PKG_DIR)/cmd.BuildTimeTag=$(shell date)" # Add the version, date, and any specified LDFLAGS to any user-specified flags. override GO_LINK_FLAGS += $(VERSION_FLAG) $(DATE_FLAG) -extldflags "$(LDFLAGS)" # Add the link flags to any user-specified flags. @@ -95,19 +98,22 @@ override GO_FLAGS += --ldflags '$(GO_LINK_FLAGS)' .PHONY: default all -default: $(NAME) $(PAM_MODULE) +default: $(NAME) $(EXT4_NAME) $(PAM_MODULE) all: format lint default test $(NAME): $(SRC_FILES) go build $(GO_FLAGS) -o $(NAME) $(CMD_PKG) +$(EXT4_NAME): $(SRC_FILES) + go build $(GO_FLAGS) -o $(EXT4_NAME) $(EXT4_PKG) + $(PAM_MODULE): $(SRC_FILES) go build -buildmode=c-shared $(GO_FLAGS) -o $(PAM_MODULE) $(PAM_PKG) rm -f $(PAM_NAME).h .PHONY: clean clean: - rm -f $(NAME) $(PAM_MODULE) $(IMAGE) + rm -f $(NAME) $(EXT4_NAME) $(PAM_MODULE) $(IMAGE) # Make sure go files build and tests pass. .PHONY: test @@ -130,8 +136,8 @@ update: # Format all the Go and C code .PHONY: format format-check format: - goimports -l -w $(GO_FILES) - clang-format -i -style=Google $(C_FILES) + @goreturns -l -w $(GO_FILES) + @clang-format -i -style=Google $(C_FILES) format-check: @goimports -d $(GO_FILES) \ @@ -148,18 +154,22 @@ lint: @megacheck -unused.exported $(GO_PKGS) ###### Installation commands ##### -.PHONY: install_bin install_pam install uninstall +.PHONY: install_bin install_ext4 install_pam install uninstall install_bin: $(NAME) $(INSTALL) -d $(DESTDIR) $(INSTALL) $(NAME) $(DESTDIR) +install_ext4: $(EXT4_NAME) + $(INSTALL) -d $(DESTDIR) + $(INSTALL) $(EXT4_NAME) $(DESTDIR) + install_pam: $(PAM_MODULE) $(INSTALL) -d $(PAM_MODULE_DIR) $(INSTALL) $(PAM_MODULE) $(PAM_MODULE_DIR) $(INSTALL) -d $(PAM_CONFIG_DIR) $(INSTALL) $(PAM_NAME)/config $(PAM_CONFIG_DIR)/$(NAME) -install: install_bin install_pam +install: install_bin install_ext4 install_pam uninstall: rm -f $(DESTDIR)/$(NAME) $(PAM_MODULE_DIR)/$(PAM_MODULE) $(PAM_CONFIG_DIR)/$(NAME) @@ -170,7 +180,7 @@ go-tools: go get -u github.com/golang/protobuf/protoc-gen-go go get -u github.com/golang/lint/golint go get -u github.com/kardianos/govendor - go get -u golang.org/x/tools/cmd/goimports + go get -u sourcegraph.com/sqs/goreturns go get -u honnef.co/go/tools/cmd/megacheck ##### Setup/Teardown for integration tests (need root permissions) ##### diff --git a/actions/config.go b/actions/config.go index 81f6e4f3..1b78af8a 100644 --- a/actions/config.go +++ b/actions/config.go @@ -73,10 +73,11 @@ func CreateConfigFile(target time.Duration, useLegacy bool) error { } defer configFile.Close() - config := &metadata.Config{ - Source: metadata.DefaultSource, - Options: metadata.DefaultOptions, - } + // config := &metadata.Config{ + // Source: metadata.DefaultSource, + // Options: metadata.DefaultOptions, + // } + config := &metadata.Config{Options: &metadata.EncryptionOptions{}} if useLegacy { config.Compatibility = LegacyConfig log.Printf("Using %q compatibility option\n", LegacyConfig) diff --git a/actions/context.go b/actions/context.go index 8ad13574..d4f54d4c 100644 --- a/actions/context.go +++ b/actions/context.go @@ -102,13 +102,11 @@ func NewContextFromMountpoint(mountpoint string, target *user.User) (*Context, e // newContextFromUser makes a context with the corresponding target user, and // whose Config is loaded from the global config file. If the target is nil, the -// effecitive user is used. +// effective user is used. func newContextFromUser(target *user.User) (*Context, error) { var err error if target == nil { - if target, err = util.EffectiveUser(); err != nil { - return nil, err - } + target = util.CurrentUser() } ctx := &Context{TargetUser: target} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 00000000..9ee1e268 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,162 @@ +/* + * cmd.go - Main interface to cmd package (Context, Command, Flag, etc...) + * + * 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 cmd is the common library for writing command line binaries. The main +// componets are the `Command`, `Context`, `Argument`, and `Flag` types which +// can be used to define a top-level command with many potential subcommands. +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +// Command represents a command with many potential sub-commands. +type Command struct { + Name string + Title string + UsageLines []string + SubCommands []*Command + InheritArguments bool + Arguments []*Argument + InheritFlags bool + Flags []Flag + ManPage *ManPage + Action Action +} + +// Run executes the command with os.Args as the provided args, equivalent to +// c.RunArgs(os.Args, helpTextMap). +func (c *Command) Run(helpTextMap map[error]string) { + c.RunArgs(os.Args, helpTextMap) +} + +// RunArgs executes the command with the provided args. If the Name argument is +// empty, args[0]'s basename is used instead. The helpTextMap provides a +// translation from error causes to explanation strings. If the command fails, +// this method will not return. +func (c *Command) RunArgs(args []string, helpTextMap map[error]string) { + binaryPath, args := args[0], args[1:] + if c.Name == "" { + c.Name = filepath.Base(binaryPath) + } + + // Create our initial context by sorting the arguments. + ctx := &Context{Command: c, helpTextMap: helpTextMap} + ctx.Args, ctx.flagArgs = sortArgs(args) + + ctx.run() +} + +// Action contains the implementation of a command. If a normal error is +// returned, the error will be printed out (with an optional explanation) and +// Run will exit with FailureCode. If a usage error is returned, the error and +// the commnd's usage are printed out and Run will exit with UsageFailureCode. +// Returning nil causes Run to return. +type Action func(ctx *Context) error + +// Context represents the state of a running application, and is the only thing +// passed to an Action. +type Context struct { + // The current command being executed + Command *Command + // The context of the parent command before this command was executed. + // Nil if this is the root context. + Parent *Context + // The non-flag arguments being passed to the command. + Args []string + // The flag arguments being passed to the command. + flagArgs []string + // The mapping of error causes to help strings + helpTextMap map[error]string +} + +// FullArguments returns the list of arguments for the current command and its +// parent arguments (if InheritArguments) is true. +func (ctx *Context) FullArguments() []*Argument { + if ctx.Parent == nil || !ctx.Command.InheritArguments { + return ctx.Command.Arguments + } + return append(ctx.Command.Arguments, ctx.Parent.FullArguments()...) +} + +// FullFlags returns the list of flags for the current command and its parent +// arguments (if InheritFlags) is true. +func (ctx *Context) FullFlags() []Flag { + if ctx.Parent == nil || !ctx.Command.InheritFlags { + return ctx.Command.Flags + } + return append(ctx.Command.Flags, ctx.Parent.FullFlags()...) +} + +// FullName returns the space-separated name of the command and all parents. +func (ctx *Context) FullName() string { + if ctx.Parent == nil { + return ctx.Command.Name + } + return fmt.Sprintf("%s %s", ctx.Parent.FullName(), ctx.Command.Name) +} + +// Info returns the same information as cmd.Info. This method only exists so +// that Info can be accessed in an output template. +func (*Context) Info() *InfoData { + return Info +} + +// ManPage returns the man page entry for this context. It is either the ManPage +// for the the current command or the closet Parent. +func (ctx *Context) ManPage() *ManPage { + if ctx.Parent == nil || ctx.Command.ManPage != nil { + return ctx.Command.ManPage + } + return ctx.Parent.ManPage() +} + +// getHelpText first tries to find a helpTextMap in either this context, or a +// parent context. Then, it looks up an error by it's cause, returning the +// appropriate help text. If no help text can be found, return an empty string. +func (ctx *Context) getHelpText(err error) string { + if ctx.helpTextMap != nil { + return ctx.helpTextMap[errors.Cause(err)] + } + if ctx.Parent == nil { + return "" + } + return ctx.Parent.getHelpText(err) +} + +// Argument represents a parameter passed to a function. It has an optional +// usage explains how it should be used. +type Argument struct { + ArgName string + Usage string +} + +func (a *Argument) String() string { return fmt.Sprintf("<%s>", a.ArgName) } + +// ManPage a man page with a name and section. +type ManPage struct { + Name string + Section int +} + +func (m *ManPage) String() string { return fmt.Sprintf("%s(%d)", m.Name, m.Section) } diff --git a/cmd/errors.go b/cmd/errors.go new file mode 100644 index 00000000..6c789125 --- /dev/null +++ b/cmd/errors.go @@ -0,0 +1,105 @@ +/* + * errors.go - Common errors and error handling + * + * 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 cmd + +import ( + "fmt" + "os" + + "github.com/pkg/errors" +) + +// Common errors used across tools +var ( + ErrUnknownVersion = errors.New("unknown version (missing version tag)") + ErrCanceled = errors.New("operation canceled by user") + ErrMustForce = errors.New("operation must be forced") +) + +// Error return codes +var ( + FailureCode = 1 + UsageFailureCode = 2 +) + +// UsageError is an error type used to denote that a command was incorrectly +// specified. Returning this type from an Action will cause print the command's +// usage to os.Stdout before exiting with UsageFailureCode. +type UsageError string + +func (u UsageError) Error() string { return string(u) } + +// CheckExpectedArgs returns a UsageError if the number of arguments in the +// context does not match expectedArgs. If atMost is set, the number of args +// is allowed to be less than expectedArgs. +func CheckExpectedArgs(ctx *Context, expectedArgs int, atMost bool) error { + // Check the number of arguments and build the message. + nArgs := len(ctx.Args) + message := "expected" + if atMost { + if nArgs <= expectedArgs { + return nil + } + message += " at most" + } else { + if nArgs == expectedArgs { + return nil + } + } + // We have the wrong number of arguments + message += fmt.Sprintf(" %s, got %s", + Pluralize(expectedArgs, "argument"), + Pluralize(nArgs, "argument")) + return UsageError(message) +} + +// CheckRequiredFlags returns a UsageError if all of the required flags are not +// set. Only StringFlags are currently supported. +func CheckRequiredFlags(flags []*StringFlag) error { + for _, flag := range flags { + if flag.Value == "" { + return UsageError(fmt.Sprintf("required flag %s not set", flag)) + } + } + return nil +} + +// processError TODO(joerichey) +func (ctx *Context) processError(err error) { + if err == nil { + return + } + + fmt.Fprintf(os.Stderr, "%s: %s\n", ctx.FullName(), err) + // Usage Errors should print the usage information + if _, ok := err.(UsageError); ok { + ExecuteTemplate(os.Stderr, TemplateUsage, ctx) + os.Exit(UsageFailureCode) + return + } + + // Errors with a help text should print it out. + if helpText := ctx.getHelpText(err); helpText != "" { + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, helpText) + } + os.Exit(FailureCode) + return +} diff --git a/cmd/ext4/ext4.go b/cmd/ext4/ext4.go new file mode 100644 index 00000000..440fd258 --- /dev/null +++ b/cmd/ext4/ext4.go @@ -0,0 +1,74 @@ +/* + * ext4.go - Handles command line processing for fscrypt-ext4. + * + * 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 ( + "fmt" + + "github.com/google/fscrypt/cmd" +) + +var ( + mountpointArg = &cmd.Argument{ + ArgName: "mountpoint", + Usage: "the mountpoint of an ext4 filesystem", + } + deviceArg = &cmd.Argument{ + ArgName: "device", + Usage: "the path to a device containing an ext4 filesystem", + } + ext4Usage = fmt.Sprintf("(%s | %s) [options]", mountpointArg, deviceArg) +) + +func main() { ext4Command.Run(nil) } + +var ext4Command = &cmd.Command{ + Title: "manage ext4 encryption feature flag", + UsageLines: []string{ + fmt.Sprintf("enable %s", ext4Usage), + fmt.Sprintf("disable %s", ext4Usage), + cmd.VersionUsage, + }, + SubCommands: []*cmd.Command{enableCommand, disableCommand, cmd.VersionCommand}, + Arguments: []*cmd.Argument{mountpointArg, deviceArg}, + Flags: []cmd.Flag{cmd.ForceFlag, cmd.VerboseFlag, cmd.HelpFlag}, + ManPage: &cmd.ManPage{Name: "fscrypt-ext4", Section: 8}, +} +var enableCommand = &cmd.Command{ + Name: "enable", + Title: "turn on encryption for an ext4 filesystem", + UsageLines: []string{ext4Usage}, + InheritArguments: true, + InheritFlags: true, + Action: func(c *cmd.Context) error { return toggleState(c, true) }, +} +var disableCommand = &cmd.Command{ + Name: "disable", + Title: "turn off encryption for an ext4 filesystem", + UsageLines: []string{ext4Usage}, + InheritArguments: true, + InheritFlags: true, + Action: func(c *cmd.Context) error { return toggleState(c, false) }, +} + +func toggleState(c *cmd.Context, enable bool) error { + fmt.Fprintf(cmd.Output, "Toggle value = %v", enable) + return nil +} diff --git a/cmd/ext4/feature_flag.go b/cmd/ext4/feature_flag.go new file mode 100644 index 00000000..58b3669b --- /dev/null +++ b/cmd/ext4/feature_flag.go @@ -0,0 +1,84 @@ +// +build linux,cgo + +/* + * feature_flag.go - Changes encryption flag for an ext4 filesystem. + * + * 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 + +/* +#cgo LDFLAGS: -lext2fs +#include +#include + +#include +*/ +import "C" +import ( + "fmt" + + "github.com/google/fscrypt/filesystem" +) + +// Ext4Filesystem wraps the C structures returned from libext2fs. +type Ext4Filesystem struct { + ptr C.ext2_filsys + mounted bool + retVal C.errcode_t +} + +// NewExt4Filesystem creates a new Ext4Filesystem from a mountpoint path. Fail +// if the path is not the mountpoint of an ext4 filesystem or cannot be opened. +func NewExt4Filesystem(mount *filesystem.Mount) (*Ext4Filesystem, error) { + if mount.Filesystem != "ext4" { + err := fmt.Errorf("%q is not an ext4 filesystem (type %q)", mount.Path, mount.Filesystem) + return nil, err + } + if mount.Device == "" { + err := fmt.Errorf("underlying device for %q is invalid", mount.Filesystem) + return nil, err + } + return nil, nil +} + +// HasValidBlockSize returns true if the filesystem has the same block size as +// the system's page size. +func (fs *Ext4Filesystem) HasValidBlockSize() bool { + return true +} + +// IsEncryptionEnabled return true if the "encrypt" feature flag is set. +func (fs *Ext4Filesystem) IsEncryptionEnabled() bool { + return C.ext2fs_has_feature_encrypt(fs.ptr.super) != 0 +} + +// EnableEncryption sets the "encrypt" feature flag and writes the appropriate +// information in the superblock to allow filesystem encryption. +func (fs *Ext4Filesystem) EnableEncryption() error { + return nil +} + +// DisableEncryption removes the "encrypt" feature flag. +func (fs *Ext4Filesystem) DisableEncryption() error { + return nil +} + +// Close safely closes, frees, and runs cleanup f9r the filesystem. +func (fs *Ext4Filesystem) Close() error { + return nil +} diff --git a/cmd/flag.go b/cmd/flag.go new file mode 100644 index 00000000..8a9d4123 --- /dev/null +++ b/cmd/flag.go @@ -0,0 +1,155 @@ +/* + * flag.go - Definitions for flags and associated formatting functions. + * + * 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 cmd + +import ( + "flag" + "fmt" + "strconv" + "time" +) + +// Useful flags that can be used with a variety of commands. +var ( + HelpFlag = &BoolFlag{ + Name: "help", + ShortName: 'h', + Usage: "Prints a help text for any command or sub-command.", + } + VerboseFlag = &BoolFlag{ + Name: "verbose", + Usage: "Prints additional debug messages.", + } + QuietFlag = &BoolFlag{ + Name: "quiet", + Usage: `Prints nothing except for errors and uses any default + option instead of prompting the user.`, + } + ForceFlag = &BoolFlag{ + Name: "force", + Usage: `Print no confirmation prompts or warnings and + automatically proceed with the requested action.`, + } +) + +// Flag represents a flag that can be passed to a command. The Name, ArgName, +// and Usage are used to format and display the flag. +type Flag interface { + // String formats the flag as either "--name" or "--name=". + fmt.Stringer + // FullUsage is the usage for this flag with an optional default note. + FullUsage() string + // Apply sets up this flag on a flag set. + Apply(*flag.FlagSet) +} + +// Formats as "--name" or as "--name=" if argName is present. +func flagFormatHelper(name, argName string) string { + if argName != "" { + return fmt.Sprintf("--%s=<%s>", name, argName) + } + return fmt.Sprintf("--%s", name) +} + +// Appends (default: ) to the usage if defaultString is present. +func flagUsageHelper(usage, defaultString string) string { + if defaultString != "" { + usage += fmt.Sprintf(" (default: %s)", defaultString) + } + return usage +} + +// BoolFlag is a Flag of type bool. +type BoolFlag struct { + Name string + ShortName byte + Usage string + Default bool + Value bool +} + +// String always uses the smaller format, as it has no ArgName. +func (f *BoolFlag) String() string { + name := f.Name + if f.ShortName != 0 { + name += ", -" + string(f.ShortName) + } + return flagFormatHelper(name, "") +} + +// FullUsage shows the default if it's true (flag is implicitly passed). +func (f *BoolFlag) FullUsage() string { + if !f.Default { + return flagUsageHelper(f.Usage, "") + } + return flagUsageHelper(f.Usage, "true") +} + +// Apply uses BoolFlag's value to set a flag.BoolVar on the FlagSet. +func (f *BoolFlag) Apply(s *flag.FlagSet) { + s.BoolVar(&f.Value, f.Name, f.Default, f.Usage) + if f.ShortName != 0 { + s.BoolVar(&f.Value, string(f.ShortName), f.Default, f.Usage) + } +} + +// StringFlag is a Flag of type string. +type StringFlag struct { + Name string + ArgName string + Usage string + Default string + Value string +} + +func (f *StringFlag) String() string { return flagFormatHelper(f.Name, f.ArgName) } + +// FullUsage shows the deafult if the string is non-empty. +func (f *StringFlag) FullUsage() string { + if f.Default == "" { + return flagUsageHelper(f.Usage, "") + } + return flagUsageHelper(f.Usage, strconv.Quote(f.Default)) +} + +// Apply uses StringFlag's value to set a flag.StringVar on the FlagSet. +func (f *StringFlag) Apply(s *flag.FlagSet) { s.StringVar(&f.Value, f.Name, f.Default, f.Usage) } + +// DurationFlag is a Flag of type time.Duration. +type DurationFlag struct { + Name string + ArgName string + Usage string + Default time.Duration + Value time.Duration +} + +func (f *DurationFlag) String() string { return flagFormatHelper(f.Name, f.ArgName) } + +// FullUsage shows the default if the duration is non-zero. +func (f *DurationFlag) FullUsage() string { + if f.Default == 0 { + return flagUsageHelper(f.Usage, "") + } + return flagUsageHelper(f.Usage, f.Default.String()) +} + +// Apply uses DurationFlag's value to set a flag.DurationVar on the FlagSet. +func (f *DurationFlag) Apply(s *flag.FlagSet) { s.DurationVar(&f.Value, f.Name, f.Default, f.Usage) } diff --git a/cmd/format.go b/cmd/format.go new file mode 100644 index 00000000..2953529d --- /dev/null +++ b/cmd/format.go @@ -0,0 +1,221 @@ +/* + * format.go - Functions for handling output formatting. + * + * 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 cmd + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "strings" + "text/template" + "unicode/utf8" + + "github.com/google/fscrypt/util" + "golang.org/x/crypto/ssh/terminal" +) + +// Suffixes for questions with a yes or no default +const ( + defaultYesSuffix = "[Y/n]" + defaultNoSuffix = "[y/N]" +) + +// Variables which control how output is formmatted and where it goes. +var ( + // TabWidth is the number of spaces used to display a tab. + TabWidth = 8 + // LineLength is the maximum length of any output. If not set, the width + // of the terminal be detected and assigned to LineLength. + LineLength int + // DefaultLineLength is the LineLength we use if we cannot detect the + // terminal width. By default we fall back to punch cards. + DefaultLineLength = 80 + // Output is the io.Writer all commands should use for their normal + // output (errors should just return the appropriate error). If not set, + // it is automatically set based on the provided flags. + Output io.Writer +) + +// We use the width of the terminal unless we cannot get the width. +func init() { + if LineLength == 0 { + var err error + LineLength, _, err = terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + LineLength = DefaultLineLength + } + } +} + +// MaxNameLength returns the length of the longest subcommand Name. Return 0 if +// there aren't subcommands. +func (c *Command) MaxNameLength() (max int) { + for _, s := range c.SubCommands { + max = util.MaxInt(max, len(s.Name)) + } + return +} + +// MaxTitleLength returns the length of the longest subcommand Title. Return 0 +// if there aren't subcommands. +func (c *Command) MaxTitleLength() (max int) { + for _, s := range c.SubCommands { + max = util.MaxInt(max, len(s.Title)) + } + return +} + +// WrapText wraps an input string so that each line begins with numTabs tabs +// (except the first line) and ends with a newline (except the last line), and +// each line has length less than lineLength. If the text contains a word which +// is too long, that word gets its own line. The first line's calculated length +// is startSpaces less (to account for strange offsets on the first line). +func WrapText(startSpaces, numTabs int, text string) string { + // We use a buffer to format the wrapped text so we get O(n) runtime + var buffer bytes.Buffer + spaceLeft := 0 + maxTextLen := LineLength - startSpaces + delimiter := strings.Repeat("\t", numTabs) + for i, word := range strings.Fields(text) { + wordLen := utf8.RuneCountInString(word) + if i == 0 { + buffer.WriteString(word) + spaceLeft = maxTextLen - wordLen + } else if wordLen >= spaceLeft { + // If no room left, write the word on the next line. + buffer.WriteString("\n") + buffer.WriteString(delimiter) + buffer.WriteString(word) + spaceLeft = maxTextLen - wordLen + } else { + // Write word on this line + buffer.WriteByte(' ') + buffer.WriteString(word) + spaceLeft -= 1 + wordLen + } + } + + return buffer.String() +} + +// Add words to this map if pluralization does not just involve adding an s. +var plurals = map[string]string{ + "policy": "policies", +} + +// Pluralize returns the correct pluralization of a work along with the +// specified count. This means Pluralize(1, "policy") = "1 policy" but +// Pluralize(2, "policy") = "2 policies". +func Pluralize(count int, word string) string { + if count == 1 { + return fmt.Sprintf("%d %s", count, word) + } + if plural, ok := plurals[word]; ok { + return fmt.Sprintf("%d %s", count, plural) + } + return fmt.Sprintf("%d %ss", count, word) +} + +// ReadLine returns a line of input from standard input. An empty string is +// returned if the user didn't insert anything, we're in quiet mode or on error. +// This function should be the only way user input is acquired from an +// application (except for passwords). +func ReadLine() (string, error) { + if QuietFlag.Value { + return "", nil + } + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + return scanner.Text(), scanner.Err() +} + +// 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) { + // Loop until failure or valid input. + for { + if defaultChoice { + fmt.Fprintf(Output, "%s %s ", question, defaultYesSuffix) + } else { + fmt.Fprintf(Output, "%s %s ", question, defaultNoSuffix) + } + + input, err := ReadLine() + 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 before performing a specific +// action. An error is returned if the user declines or IO fails. +func AskConfirmation(question, warning string, defaultChoice bool) error { + // All confirmations are "yes" if we are forcing. + if ForceFlag.Value { + return nil + } + + if warning != "" { + fmt.Fprintln(Output, "WARNING: "+warning) + } + + confirmed, err := AskQuestion(question, defaultChoice) + if err != nil { + return err + } + if !confirmed { + // To override a "false" default, use ForceFlag. + if QuietFlag.Value { + return ErrMustForce + } + return ErrCanceled + } + return nil +} + +// ExecuteTemplate creates an anonymous template from the text, and runs it with +// the provided Context and writer. Panics if text cannot be executed. +func ExecuteTemplate(w io.Writer, text string, ctx *Context) { + tmpl := template.Must(template.New("").Funcs(template.FuncMap{ + "WrapText": WrapText, + "LineLength": func() int { return LineLength }, + "add": func(nums ...int) (sum int) { + for _, num := range nums { + sum += num + } + return + }, + }).Parse(text)) + if err := tmpl.Execute(w, ctx); err != nil { + panic(err) + } +} diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index 2f23a0fc..b29435a0 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -21,99 +21,53 @@ package main import ( - "fmt" "log" "os" "github.com/pkg/errors" - "github.com/urfave/cli" "github.com/google/fscrypt/actions" - "github.com/google/fscrypt/filesystem" + "github.com/google/fscrypt/cmd" "github.com/google/fscrypt/metadata" - "github.com/google/fscrypt/security" - "github.com/google/fscrypt/util" ) -// Setup is a command which can to global or per-filesystem initialization. -var Setup = cli.Command{ - Name: "setup", - ArgsUsage: fmt.Sprintf("[%s]", mountpointArg), - Usage: "perform global setup or filesystem setup", - Description: fmt.Sprintf(`This command creates fscrypt's global config - file or enables fscrypt on a filesystem. - - (1) When used without %[1]s, create the parameters in %[2]s. - This is primarily used to configure the passphrase hashing - parameters to the appropriate hardness (as determined by %[3]s). - Being root is required to write the config file. - - (2) When used with %[1]s, enable fscrypt on %[1]s. This involves - creating the necessary folders on the filesystem which will hold - the metadata structures. Begin root may be required to create - these folders.`, mountpointArg, actions.ConfigFileLocation, - shortDisplay(timeTargetFlag)), - Flags: []cli.Flag{timeTargetFlag, legacyFlag, forceFlag}, - Action: setupAction, -} - -func setupAction(c *cli.Context) error { - var err error - switch c.NArg() { - case 0: - // Case (1) - global setup - err = createGlobalConfig(c.App.Writer, actions.ConfigFileLocation) - case 1: - // Case (2) - filesystem setup - err = setupFilesystem(c.App.Writer, c.Args().Get(0)) - default: - return expectedArgsErr(c, 1, true) - } - - if err != nil { - return newExitError(c, err) - } - return nil -} - -// Encrypt performs the functions of setupDirectory and Unlock in one command. -var Encrypt = cli.Command{ - Name: "encrypt", - ArgsUsage: directoryArg, - Usage: "enable filesystem encryption for a directory", - Description: fmt.Sprintf(`This command enables filesystem encryption on - %[1]s. This may involve creating a new policy (if one is not - specified with %[2]s) or a new protector (if one is not - specified with %[3]s). This command requires that the - corresponding filesystem has been setup with "fscrypt setup - %[4]s". By default, after %[1]s is setup, it is unlocked and can - immediately be used.`, directoryArg, shortDisplay(policyFlag), - shortDisplay(protectorFlag), mountpointArg), - Flags: []cli.Flag{policyFlag, unlockWithFlag, protectorFlag, sourceFlag, - userFlag, nameFlag, keyFileFlag, skipUnlockFlag}, - Action: encryptAction, -} - -func encryptAction(c *cli.Context) error { - if c.NArg() != 1 { - return expectedArgsErr(c, 1, false) - } - - path := c.Args().Get(0) - if err := encryptPath(path); err != nil { - return newExitError(c, err) - } - - if !skipUnlockFlag.Value { - fmt.Fprintf(c.App.Writer, - "%q is now encrypted, unlocked, and ready for use.\n", path) - } else { - fmt.Fprintf(c.App.Writer, - "%q is now encrypted, but it is still locked.\n", path) - fmt.Fprintln(c.App.Writer, `It can be unlocked with "fscrypt unlock".`) - } - return nil -} +// var Setup = cli.Command{ +// Name: "setup", +// ArgsUsage: fmt.Sprintf("[%s]", mountpointArg), +// Usage: "perform global setup or filesystem setup", +// Description: fmt.Sprintf(`This command creates fscrypt's global config +// file or enables fscrypt on a filesystem. + +// (1) When used without %[1]s, create the parameters in %[2]s. +// This is primarily used to configure the passphrase hashing +// parameters to the appropriate hardness (as determined by %[3]s). +// Being root is required to write the config file. + +// (2) When used with %[1]s, enable fscrypt on %[1]s. This involves +// creating the necessary folders on the filesystem which will hold +// the metadata structures. Begin root may be required to create +// these folders.`, mountpointArg, actions.ConfigFileLocation, +// shortDisplay(timeTargetFlag)), +// Flags: []cli.Flag{timeTargetFlag, legacyFlag, forceFlag}, +// Action: setupAction, +// } + +// var Encrypt = cli.Command{ +// Name: "encrypt", +// ArgsUsage: directoryArg, +// Usage: "enable filesystem encryption for a directory", +// Description: fmt.Sprintf(`This command enables filesystem encryption on +// %[1]s. This may involve creating a new policy (if one is not +// specified with %[2]s) or a new protector (if one is not +// specified with %[3]s). This command requires that the +// corresponding filesystem has been setup with "fscrypt setup +// %[4]s". By default, after %[1]s is setup, it is unlocked and can +// immediately be used.`, directoryArg, shortDisplay(policyFlag), +// shortDisplay(protectorFlag), mountpointArg), +// Flags: []cli.Flag{policyFlag, unlockWithFlag, protectorFlag, sourceFlag, +// userFlag, nameFlag, keyFileFlag, skipUnlockFlag}, +// Action: encryptAction, +// } // encryptPath sets up encryption on path and provisions the policy to the // keyring unless --skip-unlock is used. On failure, an error is returned, any @@ -238,7 +192,7 @@ func selectOrCreateProtector(ctx *actions.Context) (*actions.Protector, bool, er return protector, true, err } - shouldCreate, err := askQuestion("Should we create a new protector?", false) + shouldCreate, err := cmd.AskQuestion("Should we create a new protector?", false) if err != nil { return nil, false, err } @@ -252,622 +206,257 @@ func selectOrCreateProtector(ctx *actions.Context) (*actions.Protector, bool, er return protector, false, err } -// Unlock takes an encrypted directory and unlocks it for reading and writing. -var Unlock = cli.Command{ - Name: "unlock", - ArgsUsage: directoryArg, - Usage: "unlock an encrypted directory", - Description: fmt.Sprintf(`This command takes %s, a directory setup for - use with fscrypt, and unlocks the directory by passing the - appropriate key into the keyring. This requires unlocking one of - the protectors protecting this directory (either by selecting a - protector or specifying one with %s). This directory will be - locked again upon reboot, or after running "fscrypt purge" and - unmounting the corresponding filesystem.`, directoryArg, - shortDisplay(unlockWithFlag)), - Flags: []cli.Flag{unlockWithFlag, keyFileFlag, userFlag}, - Action: unlockAction, -} - -func unlockAction(c *cli.Context) error { - if c.NArg() != 1 { - return expectedArgsErr(c, 1, false) - } - +// var Unlock = cli.Command{ +// Name: "unlock", +// ArgsUsage: directoryArg, +// Usage: "unlock an encrypted directory", +// Description: fmt.Sprintf(`This command takes %s, a directory setup for +// use with fscrypt, and unlocks the directory by passing the +// appropriate key into the keyring. This requires unlocking one of +// the protectors protecting this directory (either by selecting a +// protector or specifying one with %s). This directory will be +// locked again upon reboot, or after running "fscrypt purge" and +// unmounting the corresponding filesystem.`, directoryArg, +// shortDisplay(unlockWithFlag)), +// Flags: []cli.Flag{unlockWithFlag, keyFileFlag, userFlag}, +// Action: unlockAction, +// } + +func unlockPath(path string) error { target, err := parseUserFlag(true) if err != nil { - return newExitError(c, err) + return err } - path := c.Args().Get(0) ctx, err := actions.NewContextFromPath(path, target) if err != nil { - return newExitError(c, err) + return err } log.Printf("performing sanity checks") // Ensure path is encrypted and filesystem is using fscrypt. policy, err := actions.GetPolicyFromPath(ctx, path) if err != nil { - return newExitError(c, err) + return err } // Check if directory is already unlocked if policy.IsProvisioned() { log.Printf("policy %s is already provisioned", policy.Descriptor()) - return newExitError(c, errors.Wrapf(ErrPolicyUnlocked, path)) + return errors.Wrapf(ErrPolicyUnlocked, path) } if err := policy.Unlock(optionFn, existingKeyFn); err != nil { - return newExitError(c, err) - } - defer policy.Lock() - - if err := policy.Provision(); err != nil { - return newExitError(c, err) - } - - fmt.Fprintf(c.App.Writer, "%q is now unlocked and ready for use.\n", path) - return nil -} - -// Purge removes all the policy keys from the keyring (also need unmount). -var Purge = cli.Command{ - Name: "purge", - ArgsUsage: mountpointArg, - Usage: "Remove a filesystem's keys", - Description: fmt.Sprintf(`This command removes a user's policy keys for - directories on %[1]s. This is intended to lock all files and - directories encrypted by the user on %[1]s, in that unlocking - them for reading will require providing a key again. However, - there are four important things to note about this command: - - (1) When run with the default options, this command also clears - the reclaimable dentries and inodes, so that the encrypted files - and directories will no longer be visible. However, this - requires root privileges. Note that any open file descriptors to - plaintext data will not be affected by this command. - - (2) When run with %[2]s=false, the keyring is cleared and root - permissions are not required, but recently accessed encrypted - directories and files will remain cached for some time. Because - of this, after purging a filesystem's keys in this manner, it - is recommended to unmount the filesystem. - - (3) When run as root, this command removes the policy keys for - all users. However, this will only work if the PAM module has - been enabled. Otherwise, only root's keys may be removed. - - (4) Even after unmounting the filesystem or clearing the - caches, the kernel may keep contents of files in memory. This - means direct memory access (either though physical compromise or - a kernel exploit) could compromise encrypted data. This weakness - can be eliminated by cycling the power or mitigated by using - page cache and slab cache poisoning.`, mountpointArg, - shortDisplay(dropCachesFlag)), - Flags: []cli.Flag{forceFlag, dropCachesFlag, userFlag}, - Action: purgeAction, -} - -func purgeAction(c *cli.Context) error { - if c.NArg() != 1 { - return expectedArgsErr(c, 1, false) - } - - if dropCachesFlag.Value { - if !util.IsUserRoot() { - return newExitError(c, ErrDropCachesPerm) - } - } - - target, err := parseUserFlag(true) - if err != nil { - return newExitError(c, err) - } - mountpoint := c.Args().Get(0) - ctx, err := actions.NewContextFromMountpoint(mountpoint, target) - if err != nil { - return newExitError(c, err) - } - - question := fmt.Sprintf("Purge all policy keys from %q", ctx.Mount.Path) - if dropCachesFlag.Value { - question += " and drop global inode cache" - } - warning := "Encrypted data on this filesystem will be inaccessible until unlocked again!!" - if err = askConfirmation(question+"?", false, warning); err != nil { - return newExitError(c, err) - } - - if err = actions.PurgeAllPolicies(ctx); err != nil { - return newExitError(c, err) - } - fmt.Fprintf(c.App.Writer, "Policies purged for %q.\n", ctx.Mount.Path) - - if dropCachesFlag.Value { - if err = security.DropFilesystemCache(); err != nil { - return newExitError(c, err) - } - fmt.Fprintf(c.App.Writer, "Encrypted data removed filesystem cache.\n") - } else { - fmt.Fprintf(c.App.Writer, "Filesystem %q should now be unmounted.\n", ctx.Mount.Path) - } - return nil -} - -// Status is a command with three subcommands relating to printing out status. -var Status = cli.Command{ - Name: "status", - ArgsUsage: fmt.Sprintf("[%s]", pathArg), - Usage: "print the global, filesystem, or file status", - Description: fmt.Sprintf(`This command prints out the global, - per-filesystem, or per-file status. - - (1) When used without %[1]s, print all of the currently visible - filesystems which support use with fscrypt. For each of - the filesystems, this command also notes if they are actually - being used by fscrypt. This command will fail if no there is no - support for fscrypt anywhere on the system. - - (2) When %[1]s is a filesystem mountpoint, list information - about all the policies and protectors which exist on %[1]s. This - command will fail if %[1]s is not being used with fscrypt. For - each policy, this command also notes if the policy is currently - unlocked. - - (3) When %[1]s is just a normal path, print information about - the policy being used on %[1]s and the protectors protecting - this file or directory. This command will fail if %[1]s is not - setup for encryption with fscrypt.`, pathArg), - Action: statusAction, -} - -func statusAction(c *cli.Context) error { - var err error - - switch c.NArg() { - case 0: - // Case (1) - global status - err = writeGlobalStatus(c.App.Writer) - case 1: - path := c.Args().Get(0) - ctx, mntErr := actions.NewContextFromMountpoint(path, nil) - - switch errors.Cause(mntErr) { - case nil: - // Case (2) - mountpoint status - err = writeFilesystemStatus(c.App.Writer, ctx) - case filesystem.ErrNotAMountpoint: - // Case (3) - file or directory status - err = writePathStatus(c.App.Writer, path) - default: - err = mntErr - } - default: - return expectedArgsErr(c, 1, true) - } - - if err != nil { - return newExitError(c, err) - } - return nil -} - -// Metadata is a collection of commands for manipulating the metadata files. -var Metadata = cli.Command{ - Name: "metadata", - Usage: "[ADVANCED] manipulate the policy or protector metadata", - Description: `These commands allow a user to directly create, delete, or - change the metadata files. It is important to note that using - these commands, especially the destructive ones, can make files - encrypted with fscrypt unavailable. For instance, deleting a - policy effectively deletes all the contents of the corresponding - directory. Some example use cases include: - - (1) Directly creating protectors and policies using the "create" - subcommand. These can then be applied with "fscrypt encrypt". - - (2) Changing the passphrase for a passphrase protector using the - "change-passphrase" subcommand. - - (3) Creating a policy protected with multiple protectors using - the "create policy" and "add-protector-to-policy" subcommands. - - (4) Changing the protector protecting a policy using the - "add-protector-to-policy" and "remove-protector-from-policy" - subcommands.`, - Subcommands: []cli.Command{createMetadata, destoryMetadata, changePassphrase, - addProtectorToPolicy, removeProtectorFromPolicy, dumpMetadata}, -} - -var createMetadata = cli.Command{ - Name: "create", - ArgsUsage: fmt.Sprintf("[protector | policy] %s", mountpointArg), - Usage: "manually create new metadata on a filesystem", - Subcommands: []cli.Command{createProtector, createPolicy}, -} - -var createProtector = cli.Command{ - Name: "protector", - ArgsUsage: mountpointArg, - Usage: "create a new protector on a filesystem", - Description: fmt.Sprintf(`This command creates a new protector on %s - that does not (yet) protect any policy. After creation, the user - can use %s with "fscrypt encrypt" to protect a directory with - this new protector. The creation process is identical to the - first step of "fscrypt encrypt" when the user has requested to - create a new passphrase. The user will be prompted for the - source, name, and secret data for the new protector (when - applicable). As with "fscrypt encrypt", these prompts can be - disabled with the appropriate flags.`, mountpointArg, - shortDisplay(protectorFlag)), - Flags: []cli.Flag{sourceFlag, nameFlag, keyFileFlag, userFlag}, - Action: createProtectorAction, -} - -func createProtectorAction(c *cli.Context) error { - if c.NArg() != 1 { - return expectedArgsErr(c, 1, false) - } - - target, err := parseUserFlag(false) - if err != nil { - return newExitError(c, err) - } - mountpoint := c.Args().Get(0) - ctx, err := actions.NewContextFromMountpoint(mountpoint, target) - if err != nil { - return newExitError(c, err) - } - - prompt := fmt.Sprintf("Create new protector on %q", ctx.Mount.Path) - if err := askConfirmation(prompt, true, ""); err != nil { - return newExitError(c, err) - } - - protector, err := createProtectorFromContext(ctx) - if err != nil { - return newExitError(c, err) - } - protector.Lock() - - fmt.Fprintf(c.App.Writer, "Protector %s created on filesystem %q.\n", - protector.Descriptor(), ctx.Mount.Path) - return nil -} - -var createPolicy = cli.Command{ - Name: "policy", - ArgsUsage: fmt.Sprintf("%s %s", mountpointArg, shortDisplay(protectorFlag)), - Usage: "create a new protector on a filesystem", - Description: fmt.Sprintf(`This command creates a new protector on %s - that has not (yet) been applied to any directory. After - creation, the user can use %s with "fscrypt encrypt" to encrypt - a directory with this new policy. As all policies must be - protected with at least one protector, this command requires - specifying one with %s. To create a policy protected by many - protectors, use this command and "fscrypt metadata - add-protector-to-policy".`, mountpointArg, - shortDisplay(policyFlag), shortDisplay(protectorFlag)), - Flags: []cli.Flag{protectorFlag, keyFileFlag}, - Action: createPolicyAction, -} - -func createPolicyAction(c *cli.Context) error { - if c.NArg() != 1 { - return expectedArgsErr(c, 1, false) - } - - ctx, err := actions.NewContextFromMountpoint(c.Args().Get(0), nil) - if err != nil { - return newExitError(c, err) - } - - if err := checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil { return err } - protector, err := getProtectorFromFlag(protectorFlag.Value, ctx.TargetUser) - if err != nil { - return newExitError(c, err) - } - if err := protector.Unlock(existingKeyFn); err != nil { - return newExitError(c, err) - } - defer protector.Lock() - - prompt := fmt.Sprintf("Create new policy on %q", ctx.Mount.Path) - if err := askConfirmation(prompt, true, ""); err != nil { - return newExitError(c, err) - } - - policy, err := actions.CreatePolicy(ctx, protector) - if err != nil { - return newExitError(c, err) - } - policy.Lock() - - fmt.Fprintf(c.App.Writer, "Policy %s created on filesystem %q.\n", - policy.Descriptor(), ctx.Mount.Path) - return nil -} - -var destoryMetadata = cli.Command{ - Name: "destroy", - ArgsUsage: fmt.Sprintf("[%s | %s | %s]", shortDisplay(protectorFlag), - shortDisplay(policyFlag), mountpointArg), - Usage: "delete a filesystem's, protector's, or policy's metadata", - Description: fmt.Sprintf(`This command can be used to perform three - different destructive operations. Note that in all of these - cases, data will usually be lost, so use with care. - - (1) If used with %[1]s, this command deletes all the data - associated with that protector. This means all directories - protected with that protector will become PERMANENTLY - inaccessible (unless the policies were protected by multiple - protectors). - - (2) If used with %[2]s, this command deletes all the data - associated with that policy. This means all directories (usually - just one) using this policy will become PERMANENTLY - inaccessible. - - (3) If used with %[3]s, all the metadata on that filesystem will - be deleted, causing all directories on that filesystem using - fscrypt to become PERMANENTLY inaccessible. To start using this - directory again, "fscrypt setup %[3]s" will need to be rerun.`, - shortDisplay(protectorFlag), shortDisplay(policyFlag), - mountpointArg), - Flags: []cli.Flag{protectorFlag, policyFlag, forceFlag}, - Action: destoryMetadataAction, -} - -func destoryMetadataAction(c *cli.Context) error { - switch c.NArg() { - case 0: - switch { - case protectorFlag.Value != "": - // Case (1) - protector destroy - protector, err := getProtectorFromFlag(protectorFlag.Value, nil) - if err != nil { - return newExitError(c, err) - } - - prompt := fmt.Sprintf("Destroy protector %s on %q?", - protector.Descriptor(), protector.Context.Mount.Path) - warning := "All files protected only with this protector will be lost!!" - if err := askConfirmation(prompt, false, warning); err != nil { - return newExitError(c, err) - } - if err := protector.Destroy(); err != nil { - return newExitError(c, err) - } - - fmt.Fprintf(c.App.Writer, "Protector %s deleted from filesystem %q.\n", - protector.Descriptor(), protector.Context.Mount.Path) - case policyFlag.Value != "": - // Case (2) - policy destroy - policy, err := getPolicyFromFlag(policyFlag.Value, nil) - if err != nil { - return newExitError(c, err) - } - - prompt := fmt.Sprintf("Destroy policy %s on %q?", - policy.Descriptor(), policy.Context.Mount.Path) - warning := "All files using this policy will be lost!!" - if err := askConfirmation(prompt, false, warning); err != nil { - return newExitError(c, err) - } - if err := policy.Destroy(); err != nil { - return newExitError(c, err) - } - - fmt.Fprintf(c.App.Writer, "Policy %s deleted from filesystem %q.\n", - policy.Descriptor(), policy.Context.Mount.Path) - default: - message := fmt.Sprintf("Must specify one of: %s, %s, or %s", - mountpointArg, - shortDisplay(protectorFlag), - shortDisplay(policyFlag)) - return &usageError{c, message} - } - case 1: - // Case (3) - mountpoint destroy - path := c.Args().Get(0) - ctx, err := actions.NewContextFromMountpoint(path, nil) - if err != nil { - return newExitError(c, err) - } - - prompt := fmt.Sprintf("Destroy all the metadata on %q?", ctx.Mount.Path) - warning := "All the encrypted files on this filesystem will be lost!!" - if err := askConfirmation(prompt, false, warning); err != nil { - return newExitError(c, err) - } - if err := ctx.Mount.RemoveAllMetadata(); err != nil { - return newExitError(c, err) - } - - fmt.Fprintf(c.App.Writer, "All metadata on %q deleted.\n", ctx.Mount.Path) - default: - return expectedArgsErr(c, 1, true) - } - return nil -} - -var changePassphrase = cli.Command{ - Name: "change-passphrase", - ArgsUsage: shortDisplay(protectorFlag), - Usage: "change the passphrase used for a protector", - Description: `This command takes a specified passphrase protector and - changes the corresponding passphrase. Note that this does not - create or destroy any protectors.`, - Flags: []cli.Flag{protectorFlag}, - Action: changePassphraseAction, -} - -func changePassphraseAction(c *cli.Context) error { - if c.NArg() != 0 { - return expectedArgsErr(c, 0, false) - } - if err := checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil { - return err - } - - protector, err := getProtectorFromFlag(protectorFlag.Value, nil) - if err != nil { - return newExitError(c, err) - } - if err := protector.Unlock(oldExistingKeyFn); err != nil { - return newExitError(c, err) - } - defer protector.Lock() - if err := protector.Rewrap(newCreateKeyFn); err != nil { - return newExitError(c, err) - } - - fmt.Fprintf(c.App.Writer, "Passphrase for protector %s successfully changed.\n", - protector.Descriptor()) - return nil -} - -var addProtectorToPolicy = cli.Command{ - Name: "add-protector-to-policy", - ArgsUsage: fmt.Sprintf("%s %s", shortDisplay(protectorFlag), shortDisplay(policyFlag)), - Usage: "start protecting a policy with some protector", - Description: `This command changes the specified policy to be - protected with the specified protector. This means that any - directories using this policy will now be accessible with this - protector. This command will fail if the policy is already - protected with this protector.`, - Flags: []cli.Flag{protectorFlag, policyFlag, unlockWithFlag, keyFileFlag}, - Action: addProtectorAction, -} - -func addProtectorAction(c *cli.Context) error { - if c.NArg() != 0 { - return expectedArgsErr(c, 0, false) - } - if err := checkRequiredFlags(c, []*stringFlag{protectorFlag, policyFlag}); err != nil { - return err - } - - protector, err := getProtectorFromFlag(protectorFlag.Value, nil) - if err != nil { - return newExitError(c, err) - } - policy, err := getPolicyFromFlag(policyFlag.Value, protector.Context.TargetUser) - if err != nil { - return newExitError(c, err) - } - // Sanity check before unlocking everything - if err := policy.AddProtector(protector); errors.Cause(err) != actions.ErrLocked { - return newExitError(c, err) - } - - prompt := fmt.Sprintf("Protect policy %s with protector %s?", - policy.Descriptor(), protector.Descriptor()) - warning := "All files using this policy will be accessible with this protector!!" - if err := askConfirmation(prompt, true, warning); err != nil { - return newExitError(c, err) - } - - if err := protector.Unlock(existingKeyFn); err != nil { - return newExitError(c, err) - } - if err := policy.Unlock(optionFn, existingKeyFn); err != nil { - return newExitError(c, err) - } - if err := policy.AddProtector(protector); err != nil { - return newExitError(c, err) - } - - fmt.Fprintf(c.App.Writer, "Protector %s now protecting policy %s.\n", - protector.Descriptor(), policy.Descriptor()) - return nil -} - -var removeProtectorFromPolicy = cli.Command{ - Name: "remove-protector-from-policy", - ArgsUsage: fmt.Sprintf("%s %s", shortDisplay(protectorFlag), shortDisplay(policyFlag)), - Usage: "stop protecting a policy with some protector", - Description: `This command changes the specified policy to no longer be - protected with the specified protector. This means that any - directories using this policy will cannot be accessed with this - protector. This command will fail if the policy not already - protected with this protector or if it is the policy's only - protector.`, - Flags: []cli.Flag{protectorFlag, policyFlag, forceFlag}, - Action: removeProtectorAction, -} - -func removeProtectorAction(c *cli.Context) error { - if c.NArg() != 0 { - return expectedArgsErr(c, 0, false) - } - if err := checkRequiredFlags(c, []*stringFlag{protectorFlag, policyFlag}); err != nil { - return err - } - - // We do not need to unlock anything for this operation - protector, err := getProtectorFromFlag(protectorFlag.Value, nil) - if err != nil { - return newExitError(c, err) - } - policy, err := getPolicyFromFlag(policyFlag.Value, protector.Context.TargetUser) - if err != nil { - return newExitError(c, err) - } - - prompt := fmt.Sprintf("Stop protecting policy %s with protector %s?", - policy.Descriptor(), protector.Descriptor()) - warning := "All files using this policy will NO LONGER be accessible with this protector!!" - if err := askConfirmation(prompt, false, warning); err != nil { - return newExitError(c, err) - } - - if err := policy.RemoveProtector(protector); err != nil { - return newExitError(c, err) - } - - fmt.Fprintf(c.App.Writer, "Protector %s no longer protecting policy %s.\n", - protector.Descriptor(), policy.Descriptor()) - return nil -} + defer policy.Lock() -var dumpMetadata = cli.Command{ - Name: "dump", - ArgsUsage: fmt.Sprintf("[%s | %s]", shortDisplay(protectorFlag), shortDisplay(policyFlag)), - Usage: "print debug data for a policy or protector", - Description: fmt.Sprintf(`This commands dumps all of the debug data for - a protector (if %s is used) or policy (if %s is used). This data - includes the data pulled from the %q config file, the - appropriate mountpoint data, and any options for the policy or - hashing costs for the protector. Any cryptographic keys are - wiped and are not printed out.`, shortDisplay(protectorFlag), - shortDisplay(policyFlag), actions.ConfigFileLocation), - Flags: []cli.Flag{protectorFlag, policyFlag}, - Action: dumpMetadataAction, + return policy.Provision() } -func dumpMetadataAction(c *cli.Context) error { - switch { - case protectorFlag.Value != "": - // Case (1) - protector print - protector, err := getProtectorFromFlag(protectorFlag.Value, nil) - if err != nil { - return newExitError(c, err) - } - fmt.Fprintln(c.App.Writer, protector) - case policyFlag.Value != "": - // Case (2) - policy print - policy, err := getPolicyFromFlag(policyFlag.Value, nil) - if err != nil { - return newExitError(c, err) - } - fmt.Fprintln(c.App.Writer, policy) - default: - message := fmt.Sprintf("Must specify one of: %s or %s", - shortDisplay(protectorFlag), - shortDisplay(policyFlag)) - return &usageError{c, message} - } - return nil -} +// var Purge = cli.Command{ +// Name: "purge", +// ArgsUsage: mountpointArg, +// Usage: "Remove a filesystem's keys", +// Description: fmt.Sprintf(`This command removes a user's policy keys for +// directories on %[1]s. This is intended to lock all files and +// directories encrypted by the user on %[1]s, in that unlocking +// them for reading will require providing a key again. However, +// there are four important things to note about this command: + +// (1) When run with the default options, this command also clears +// the reclaimable dentries and inodes, so that the encrypted files +// and directories will no longer be visible. However, this +// requires root privileges. Note that any open file descriptors to +// plaintext data will not be affected by this command. + +// (2) When run with %[2]s=false, the keyring is cleared and root +// permissions are not required, but recently accessed encrypted +// directories and files will remain cached for some time. Because +// of this, after purging a filesystem's keys in this manner, it +// is recommended to unmount the filesystem. + +// (3) When run as root, this command removes the policy keys for +// all users. However, this will only work if the PAM module has +// been enabled. Otherwise, only root's keys may be removed. + +// (4) Even after unmounting the filesystem or clearing the +// caches, the kernel may keep contents of files in memory. This +// means direct memory access (either though physical compromise or +// a kernel exploit) could compromise encrypted data. This weakness +// can be eliminated by cycling the power or mitigated by using +// page cache and slab cache poisoning.`, mountpointArg, +// shortDisplay(dropCachesFlag)), +// Flags: []cli.Flag{forceFlag, dropCachesFlag, userFlag}, +// Action: purgeAction, +// } + +// var Status = cli.Command{ +// Name: "status", +// ArgsUsage: fmt.Sprintf("[%s]", pathArg), +// Usage: "print the global, filesystem, or file status", +// Description: fmt.Sprintf(`This command prints out the global, +// per-filesystem, or per-file status. + +// (1) When used without %[1]s, print all of the currently visible +// filesystems which support use with fscrypt. For each of +// the filesystems, this command also notes if they are actually +// being used by fscrypt. This command will fail if no there is no +// support for fscrypt anywhere on the system. + +// (2) When %[1]s is a filesystem mountpoint, list information +// about all the policies and protectors which exist on %[1]s. This +// command will fail if %[1]s is not being used with fscrypt. For +// each policy, this command also notes if the policy is currently +// unlocked. + +// (3) When %[1]s is just a normal path, print information about +// the policy being used on %[1]s and the protectors protecting +// this file or directory. This command will fail if %[1]s is not +// setup for encryption with fscrypt.`, pathArg), +// Action: statusAction, +// } + +// var Metadata = cli.Command{ +// Name: "metadata", +// Usage: "[ADVANCED] manipulate the policy or protector metadata", +// Description: `These commands allow a user to directly create, delete, or +// change the metadata files. It is important to note that using +// these commands, especially the destructive ones, can make files +// encrypted with fscrypt unavailable. For instance, deleting a +// policy effectively deletes all the contents of the corresponding +// directory. Some example use cases include: + +// (1) Directly creating protectors and policies using the "create" +// subcommand. These can then be applied with "fscrypt encrypt". + +// (2) Changing the passphrase for a passphrase protector using the +// "change-passphrase" subcommand. + +// (3) Creating a policy protected with multiple protectors using +// the "create policy" and "add-protector-to-policy" subcommands. + +// (4) Changing the protector protecting a policy using the +// "add-protector-to-policy" and "remove-protector-from-policy" +// subcommands.`, +// Subcommands: []cli.Command{createMetadata, destoryMetadata, changePassphrase, +// addProtectorToPolicy, removeProtectorFromPolicy, dumpMetadata}, +// } + +// var createProtector = cli.Command{ +// Name: "protector", +// ArgsUsage: mountpointArg, +// Usage: "create a new protector on a filesystem", +// Description: fmt.Sprintf(`This command creates a new protector on %s +// that does not (yet) protect any policy. After creation, the user +// can use %s with "fscrypt encrypt" to protect a directory with +// this new protector. The creation process is identical to the +// first step of "fscrypt encrypt" when the user has requested to +// create a new passphrase. The user will be prompted for the +// source, name, and secret data for the new protector (when +// applicable). As with "fscrypt encrypt", these prompts can be +// disabled with the appropriate flags.`, mountpointArg, +// shortDisplay(protectorFlag)), +// Flags: []cli.Flag{sourceFlag, nameFlag, keyFileFlag, userFlag}, +// Action: createProtectorAction, +// } + +// var createPolicy = cli.Command{ +// Name: "policy", +// ArgsUsage: fmt.Sprintf("%s %s", mountpointArg, shortDisplay(protectorFlag)), +// Usage: "create a new protector on a filesystem", +// Description: fmt.Sprintf(`This command creates a new protector on %s +// that has not (yet) been applied to any directory. After +// creation, the user can use %s with "fscrypt encrypt" to encrypt +// a directory with this new policy. As all policies must be +// protected with at least one protector, this command requires +// specifying one with %s. To create a policy protected by many +// protectors, use this command and "fscrypt metadata +// add-protector-to-policy".`, mountpointArg, +// shortDisplay(policyFlag), shortDisplay(protectorFlag)), +// Flags: []cli.Flag{protectorFlag, keyFileFlag}, +// Action: createPolicyAction, +// } + +//// ***** WIP END/BEGIN HERE ***** + +// var destoryMetadata = cli.Command{ +// Name: "destroy", +// ArgsUsage: fmt.Sprintf("[%s | %s | %s]", shortDisplay(protectorFlag), +// shortDisplay(policyFlag), mountpointArg), +// Usage: "delete a filesystem's, protector's, or policy's metadata", +// Description: fmt.Sprintf(`This command can be used to perform three +// different destructive operations. Note that in all of these +// cases, data will usually be lost, so use with care. + +// (1) If used with %[1]s, this command deletes all the data +// associated with that protector. This means all directories +// protected with that protector will become PERMANENTLY +// inaccessible (unless the policies were protected by multiple +// protectors). + +// (2) If used with %[2]s, this command deletes all the data +// associated with that policy. This means all directories (usually +// just one) using this policy will become PERMANENTLY +// inaccessible. + +// (3) If used with %[3]s, all the metadata on that filesystem will +// be deleted, causing all directories on that filesystem using +// fscrypt to become PERMANENTLY inaccessible. To start using this +// directory again, "fscrypt setup %[3]s" will need to be rerun.`, +// shortDisplay(protectorFlag), shortDisplay(policyFlag), +// mountpointArg), +// Flags: []cli.Flag{protectorFlag, policyFlag, forceFlag}, +// Action: destoryMetadataAction, +// } + +// var changePassphrase = cli.Command{ +// Name: "change-passphrase", +// ArgsUsage: shortDisplay(protectorFlag), +// Usage: "change the passphrase used for a protector", +// Description: `This command takes a specified passphrase protector and +// changes the corresponding passphrase. Note that this does not +// create or destroy any protectors.`, +// Flags: []cli.Flag{protectorFlag}, +// Action: changePassphraseAction, +// } + +// var addProtectorToPolicy = cli.Command{ +// Name: "add-protector-to-policy", +// ArgsUsage: fmt.Sprintf("%s %s", shortDisplay(protectorFlag), shortDisplay(policyFlag)), +// Usage: "start protecting a policy with some protector", +// Description: `This command changes the specified policy to be +// protected with the specified protector. This means that any +// directories using this policy will now be accessible with this +// protector. This command will fail if the policy is already +// protected with this protector.`, +// Flags: []cli.Flag{protectorFlag, policyFlag, unlockWithFlag, keyFileFlag}, +// Action: addProtectorAction, +// } + +// var removeProtectorFromPolicy = cli.Command{ +// Name: "remove-protector-from-policy", +// ArgsUsage: fmt.Sprintf("%s %s", shortDisplay(protectorFlag), shortDisplay(policyFlag)), +// Usage: "stop protecting a policy with some protector", +// Description: `This command changes the specified policy to no longer be +// protected with the specified protector. This means that any +// directories using this policy will cannot be accessed with this +// protector. This command will fail if the policy not already +// protected with this protector or if it is the policy's only +// protector.`, +// Flags: []cli.Flag{protectorFlag, policyFlag, forceFlag}, +// Action: removeProtectorAction, +// } + +// var dumpMetadata = cli.Command{ +// Name: "dump", +// ArgsUsage: fmt.Sprintf("[%s | %s]", shortDisplay(protectorFlag), shortDisplay(policyFlag)), +// Usage: "print debug data for a policy or protector", +// Description: fmt.Sprintf(`This commands dumps all of the debug data for +// a protector (if %s is used) or policy (if %s is used). This data +// includes the data pulled from the %q config file, the +// appropriate mountpoint data, and any options for the policy or +// hashing costs for the protector. Any cryptographic keys are +// wiped and are not printed out.`, shortDisplay(protectorFlag), +// shortDisplay(policyFlag), actions.ConfigFileLocation), +// Flags: []cli.Flag{protectorFlag, policyFlag}, +// Action: dumpMetadataAction, +// } diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go index 81a67985..4489fe58 100644 --- a/cmd/fscrypt/errors.go +++ b/cmd/fscrypt/errors.go @@ -22,16 +22,12 @@ package main import ( - "bytes" "fmt" - "os" - "path/filepath" - "unicode/utf8" "github.com/pkg/errors" - "github.com/urfave/cli" "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/cmd" "github.com/google/fscrypt/crypto" "github.com/google/fscrypt/filesystem" "github.com/google/fscrypt/metadata" @@ -44,8 +40,6 @@ const failureExitCode = 1 // Various errors used for the top level user interface var ( - ErrCanceled = errors.New("operation canceled") - ErrNoDesctructiveOps = errors.New("operation would be destructive") ErrMaxPassphrase = util.SystemError("max passphrase length exceeded") ErrInvalidSource = errors.New("invalid source type") ErrPassphraseMismatch = errors.New("entered passphrases do not match") @@ -54,7 +48,6 @@ var ( ErrSpecifyKeyFile = errors.New("no key file specified") ErrKeyFileLength = errors.Errorf("key file must be %d bytes", metadata.InternalKeyLen) ErrAllLoadsFailed = errors.New("could not load any protectors") - ErrMustBeRoot = errors.New("this command must be run as root") ErrPolicyUnlocked = errors.New("this file or directory is already unlocked") ErrBadOwners = errors.New("you do not own this directory") ErrNotEmptyDir = errors.New("not an empty directory") @@ -64,14 +57,10 @@ var ( ErrSpecifyUser = errors.New("user must be specified when run as root") ) -var loadHelpText = fmt.Sprintf("You may need to mount a linked filesystem. Run with %s for more information.", shortDisplay(verboseFlag)) +var loadHelpText = fmt.Sprintf("You may need to mount a linked filesystem. Run with %s for more information.", cmd.VerboseFlag) -// getFullName returns the full name of the application or command being used. -func getFullName(c *cli.Context) string { - if c.Command.HelpName != "" { - return c.Command.HelpName - } - return c.App.HelpName +var fscryptHelpTextMap = map[error]string{ + actions.ErrBadConfigFile: `Run "sudo fscrypt setup" to recreate the file.`, } // getErrorSuggestions returns a string containing suggestions about how to fix @@ -79,7 +68,7 @@ func getFullName(c *cli.Context) string { func getErrorSuggestions(err error) string { switch errors.Cause(err) { case filesystem.ErrNotSetup: - return fmt.Sprintf(`Run "fscrypt setup %s" to use fscrypt on this filesystem.`, mountpointArg) + return fmt.Sprintf(`Run "fscrypt setup %s" to use fscrypt on this filesystem.`, unusedMountpointArg) case crypto.ErrKeyLock: return `Too much memory was requested to be locked in RAM. The current limit for this user can be checked with "ulimit @@ -101,9 +90,7 @@ func getErrorSuggestions(err error) string { case security.ErrAccessUserKeyring: return fmt.Sprintf(`You can only use %s to access the user keyring of another user if you are running as root.`, - shortDisplay(userFlag)) - case actions.ErrBadConfigFile: - return `Run "sudo fscrypt setup" to recreate the file.` + userFlag) case actions.ErrNoConfigFile: return `Run "sudo fscrypt setup" to create the file.` case actions.ErrMissingPolicyMetadata: @@ -115,13 +102,13 @@ func getErrorSuggestions(err error) string { inconsistent state. This most likely means the filesystem metadata is corrupted.` case actions.ErrMissingProtectorName: - return fmt.Sprintf("Use %s to specify a protector name.", shortDisplay(nameFlag)) - case ErrNoDesctructiveOps: - return fmt.Sprintf("Use %s to automatically run destructive operations.", shortDisplay(forceFlag)) + return fmt.Sprintf("Use %s to specify a protector name.", nameFlag) + case cmd.ErrMustForce: + return fmt.Sprintf("Use %s to automatically run destructive operations.", cmd.ForceFlag) case ErrSpecifyProtector: - return fmt.Sprintf("Use %s to specify a protector.", shortDisplay(protectorFlag)) + return fmt.Sprintf("Use %s to specify a protector.", protectorFlag) case ErrSpecifyKeyFile: - return fmt.Sprintf("Use %s to specify a key file.", shortDisplay(keyFileFlag)) + return fmt.Sprintf("Use %s to specify a key file.", keyFileFlag) case ErrBadOwners: return `Encryption can only be setup on directories you own, even if you have write permission for the directory.` @@ -134,7 +121,7 @@ func getErrorSuggestions(err error) string { return fmt.Sprintf(`Either this command should be run as root to properly clear the inode cache, or it should be run with %s=false (this may leave encrypted files and directories - in an accessible state).`, shortDisplay(dropCachesFlag)) + in an accessible state).`, dropCachesFlag) case ErrSpecifyUser: return fmt.Sprintf(`When running this command as root, you usually still want to provision/remove keys for a normal @@ -142,89 +129,10 @@ func getErrorSuggestions(err error) string { as a protector (so the corresponding files will be accessible for that user). This can be done with %s. To use the root user's keyring or passphrase, use - --%s=root.`, shortDisplay(userFlag), userFlag.GetName()) + --%s=root.`, userFlag, userFlag.Name) case ErrAllLoadsFailed: return loadHelpText default: return "" } } - -// newExitError creates a new error for a given context and normal error. The -// returned error prepends the name of the relevant command and will make -// fscrypt return a non-zero exit value. -func newExitError(c *cli.Context, err error) error { - // Prepend the full name and append suggestions (if any) - fullNamePrefix := getFullName(c) + ": " - message := fullNamePrefix + wrapText(err.Error(), utf8.RuneCountInString(fullNamePrefix)) - - if suggestion := getErrorSuggestions(err); suggestion != "" { - message += "\n\n" + wrapText(suggestion, 0) - } - - return cli.NewExitError(message, failureExitCode) -} - -// usageError implements cli.ExitCoder to will print the usage and the return a -// non-zero value. This error should be used when a command is used incorrectly. -type usageError struct { - c *cli.Context - message string -} - -func (u *usageError) Error() string { - return fmt.Sprintf("%s: %s", getFullName(u.c), u.message) -} - -// We get the help to print after the error by having it run right before the -// application exits. This is very nasty, but there isn't a better way to do it -// with the constraints of urfave/cli. -func (u *usageError) ExitCode() int { - // Redirect help output to a buffer, so we can customize it. - buf := new(bytes.Buffer) - oldWriter := u.c.App.Writer - u.c.App.Writer = buf - - // Get the appropriate help - if getFullName(u.c) == filepath.Base(os.Args[0]) { - cli.ShowAppHelp(u.c) - } else { - cli.ShowCommandHelp(u.c, u.c.Command.Name) - } - - // Remove first line from help and print it out - buf.ReadBytes('\n') - buf.WriteTo(oldWriter) - u.c.App.Writer = oldWriter - return failureExitCode -} - -// expectedArgsErr creates a usage error for the incorrect number of arguments -// being specified. atMost should be true only if any number of arguments from 0 -// to expectedArgs would be acceptable. -func expectedArgsErr(c *cli.Context, expectedArgs int, atMost bool) error { - message := "expected " - if atMost { - message += "at most " - } - message += fmt.Sprintf("%s, got %s", - pluralize(expectedArgs, "argument"), pluralize(c.NArg(), "argument")) - return &usageError{c, message} -} - -// onUsageError is a function handler for the application and each command. -func onUsageError(c *cli.Context, err error, _ bool) error { - return &usageError{c, err.Error()} -} - -// checkRequiredFlags makes sure that all of the specified string flags have -// been given nonempty values. Returns a usage error on failure. -func checkRequiredFlags(c *cli.Context, flags []*stringFlag) error { - for _, flag := range flags { - if flag.Value == "" { - message := fmt.Sprintf("required flag %s not provided", shortDisplay(flag)) - return &usageError{c, message} - } - } - return nil -} diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go index 5137effb..084fa193 100644 --- a/cmd/fscrypt/flags.go +++ b/cmd/fscrypt/flags.go @@ -22,230 +22,97 @@ package main import ( - "flag" "fmt" "log" "os/user" "regexp" - "strconv" "time" - "github.com/urfave/cli" - "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/cmd" "github.com/google/fscrypt/security" "github.com/google/fscrypt/util" ) -// We define the types boolFlag, durationFlag, and stringFlag here instead of -// using those present in urfave/cli because we need them to conform to the -// prettyFlag interface (in format.go). The Getters just get the corresponding -// variables, String() just uses longDisplay, and Apply just sets the -// corresponding type of flag. -type boolFlag struct { - Name string - Usage string - Default bool - Value bool -} - -func (b *boolFlag) GetName() string { return b.Name } -func (b *boolFlag) GetArgName() string { return "" } -func (b *boolFlag) GetUsage() string { return b.Usage } - -func (b *boolFlag) String() string { - if !b.Default { - return longDisplay(b) - } - return longDisplay(b, strconv.FormatBool(b.Default)) -} - -func (b *boolFlag) Apply(set *flag.FlagSet) { - set.BoolVar(&b.Value, b.Name, b.Default, b.Usage) -} - -type durationFlag struct { - Name string - ArgName string - Usage string - Default time.Duration - Value time.Duration -} - -func (d *durationFlag) GetName() string { return d.Name } -func (d *durationFlag) GetArgName() string { return d.ArgName } -func (d *durationFlag) GetUsage() string { return d.Usage } - -func (d *durationFlag) String() string { - if d.Default == 0 { - return longDisplay(d) - } - return longDisplay(d, d.Value.String()) -} - -func (d *durationFlag) Apply(set *flag.FlagSet) { - set.DurationVar(&d.Value, d.Name, d.Default, d.Usage) -} - -type stringFlag struct { - Name string - ArgName string - Usage string - Default string - Value string -} - -func (s *stringFlag) GetName() string { return s.Name } -func (s *stringFlag) GetArgName() string { return s.ArgName } -func (s *stringFlag) GetUsage() string { return s.Usage } - -func (s *stringFlag) String() string { - if s.Default == "" { - return longDisplay(s) - } - return longDisplay(s, strconv.Quote(s.Default)) -} - -func (s *stringFlag) Apply(set *flag.FlagSet) { - set.StringVar(&s.Value, s.Name, s.Default, s.Usage) -} - -var ( - // allFlags contains every defined flag (used for formatting). - // UPDATE THIS ARRAY WHEN ADDING NEW FLAGS!!! - // TODO(joerichey) add presubmit rule to enforce this - allFlags = []prettyFlag{helpFlag, versionFlag, verboseFlag, quietFlag, - forceFlag, legacyFlag, skipUnlockFlag, timeTargetFlag, - sourceFlag, nameFlag, keyFileFlag, protectorFlag, - unlockWithFlag, policyFlag} - // universalFlags contains flags that should be on every command - universalFlags = []cli.Flag{verboseFlag, quietFlag, helpFlag} -) - // Bool flags: used to switch some behavior on or off var ( - helpFlag = &boolFlag{ - Name: "help", - Usage: `Prints help screen for commands and subcommands.`, - } - versionFlag = &boolFlag{ - Name: "version", - Usage: `Prints version and license information.`, - } - verboseFlag = &boolFlag{ - Name: "verbose", - Usage: `Prints additional debug messages to standard output.`, - } - quietFlag = &boolFlag{ - Name: "quiet", - Usage: `Prints nothing to standard output except for errors. - Selects the default for any options that would normally - show a prompt.`, - } - forceFlag = &boolFlag{ - Name: "force", - Usage: fmt.Sprintf(`Suppresses all confirmation prompts and - warnings, causing any action to automatically proceed. - WARNING: This bypasses confirmations for protective - operations, use with care.`), - } - legacyFlag = &boolFlag{ - Name: "legacy", - Usage: `Allow for support of older kernels with ext4 (before - v4.8) and F2FS (before v4.6) filesystems.`, + legacyFlag = &cmd.BoolFlag{ + Name: "legacy", + Usage: `Configure fscrypt to support older kernels.`, Default: true, } - skipUnlockFlag = &boolFlag{ - Name: "skip-unlock", - Usage: `Leave the directory in a locked state after setup. - "fscrypt unlock" will need to be run in order to use the - directory.`, + skipUnlockFlag = &cmd.BoolFlag{ + Name: "skip-unlock", + Usage: "Leave the directory in a locked state after setup.", } - dropCachesFlag = &boolFlag{ + dropCachesFlag = &cmd.BoolFlag{ Name: "drop-caches", Usage: `After purging the keys from the keyring, drop the - associated caches for the purge to take effect. Without - this flag, cached encrypted files may still have their - plaintext visible. Requires root privileges.`, + associated caches for the purge to take effect.`, Default: true, } ) // Option flags: used to specify options instead of being prompted for them var ( - timeTargetFlag = &durationFlag{ + timeTargetFlag = &cmd.DurationFlag{ Name: "time", - ArgName: "TIME", + ArgName: "time", Usage: `Set the global options so that passphrase hashing takes - TIME long. TIME should be formatted as a sequence of - decimal numbers, each with optional fraction and a unit - suffix, such as "300ms", "1.5s" or "2h45m". Valid time - units are "ms", "s", "m", and "h".`, +