From bab7dfdf68075b345e4de3ae79ea685ca884668f Mon Sep 17 00:00:00 2001 From: "Joe Richey joerichey@google.com" Date: Tue, 17 Oct 2017 02:39:07 -0700 Subject: Move around and fscrypt refactor --- cmd/cmd.go | 30 ++-- cmd/errors.go | 8 +- cmd/ext4/ext4.go | 74 ++++++++++ cmd/ext4/feature_flag.go | 84 +++++++++++ cmd/format.go | 14 +- cmd/fscrypt/commands.go | 378 +++++++++++++++-------------------------------- cmd/fscrypt/errors.go | 60 +------- cmd/fscrypt/flags.go | 23 +-- cmd/fscrypt/fscrypt.go | 188 ++++++++++++++++++++++- cmd/fscrypt/prompt.go | 29 ---- cmd/fscrypt/strings.go | 52 ------- cmd/run.go | 4 +- 12 files changed, 504 insertions(+), 440 deletions(-) create mode 100644 cmd/ext4/ext4.go create mode 100644 cmd/ext4/feature_flag.go delete mode 100644 cmd/fscrypt/strings.go (limited to 'cmd') diff --git a/cmd/cmd.go b/cmd/cmd.go index 3358015..9ee1e26 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -44,22 +44,24 @@ type Command struct { Action Action } -// Run executes the command with os.Args, equivalent to c.RunArgs(os.Args). -func (c *Command) Run() { - c.RunArgs(os.Args) +// 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. If the command fails, this method -// will not return. -func (c *Command) RunArgs(args []string) { +// 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} + ctx := &Context{Command: c, helpTextMap: helpTextMap} ctx.Args, ctx.flagArgs = sortArgs(args) ctx.run() @@ -85,7 +87,7 @@ type Context struct { // The flag arguments being passed to the command. flagArgs []string // The mapping of error causes to help strings - errorMap map[error]string + helpTextMap map[error]string } // FullArguments returns the list of arguments for the current command and its @@ -129,15 +131,17 @@ func (ctx *Context) ManPage() *ManPage { return ctx.Parent.ManPage() } -// getHelp tries to find a helpMap and then lookup the error by it's cause. -func (ctx *Context) getHelp(err error) string { - if ctx.errorMap != nil { - return ctx.errorMap[errors.Cause(err)] +// 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.getHelp(err) + return ctx.Parent.getHelpText(err) } // Argument represents a parameter passed to a function. It has an optional diff --git a/cmd/errors.go b/cmd/errors.go index 0252fed..d4aca6d 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -49,11 +49,11 @@ type UsageError string func (u UsageError) Error() string { return string(u) } -// CheckExpectedArgs returns a UsageError if the number of arguements in the +// 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 arguements and build the message. + // Check the number of arguments and build the message. nArgs := len(ctx.Args) message := "expected" if atMost { @@ -107,9 +107,9 @@ func (ctx *Context) processError(err error) { } // Errors with a help text should print it out. - if helpText := ctx.getHelp(err); helpText != "" { + if helpText := ctx.getHelpText(err); helpText != "" { fmt.Fprintln(os.Stderr) - fmt.Fprintln(os.Stderr, wrapText(helpText, 0)) + fmt.Fprintln(os.Stderr, WrapText(helpText, 0)) } os.Exit(FailureCode) return diff --git a/cmd/ext4/ext4.go b/cmd/ext4/ext4.go new file mode 100644 index 0000000..1df1f23 --- /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() } + +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 0000000..58b3669 --- /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/format.go b/cmd/format.go index 69fd0e9..877938c 100644 --- a/cmd/format.go +++ b/cmd/format.go @@ -69,11 +69,11 @@ func init() { } } -// Takes an input string text, and wraps the text 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. -func wrapText(text string, numTabs int) string { +// 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. +func WrapText(text string, numTabs int) string { // We use a buffer to format the wrapped text so we get O(n) runtime var buffer bytes.Buffer spaceLeft := 0 @@ -168,7 +168,7 @@ func AskConfirmation(question, warning string, defaultChoice bool) error { } if warning != "" { - fmt.Fprintln(Output, wrapText("WARNING: "+warning, 0)) + fmt.Fprintln(Output, WrapText("WARNING: "+warning, 0)) } confirmed, err := AskQuestion(question, defaultChoice) @@ -185,7 +185,7 @@ func AskConfirmation(question, warning string, defaultChoice bool) error { // 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, + "WrapText": WrapText, }).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 e8d32da..4eec1e6 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -29,90 +29,46 @@ import ( "github.com/urfave/cli" "github.com/google/fscrypt/actions" - "github.com/google/fscrypt/filesystem" "github.com/google/fscrypt/metadata" - "github.com/google/fscrypt/security" - "github.com/google/fscrypt/util" ) -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 @@ -251,204 +207,114 @@ 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) + return 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, + return policy.Provision() } -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 -} +// 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, +// } // Metadata is a collection of commands for manipulating the metadata files. var Metadata = cli.Command{ diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go index 4ce6133..1c7ee75 100644 --- a/cmd/fscrypt/errors.go +++ b/cmd/fscrypt/errors.go @@ -23,10 +23,8 @@ package main import ( "fmt" - "unicode/utf8" "github.com/pkg/errors" - "github.com/urfave/cli" "github.com/google/fscrypt/actions" "github.com/google/fscrypt/crypto" @@ -41,8 +39,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") @@ -51,7 +47,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") @@ -63,12 +58,8 @@ var ( var loadHelpText = fmt.Sprintf("You may need to mount a linked filesystem. Run with %s for more information.", shortDisplay(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 @@ -99,8 +90,6 @@ func getErrorSuggestions(err error) string { 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.` case actions.ErrNoConfigFile: return `Run "sudo fscrypt setup" to create the file.` case actions.ErrMissingPolicyMetadata: @@ -146,48 +135,3 @@ func getErrorSuggestions(err error) string { 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) -} - -// 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 69126bc..2bf7f73 100644 --- a/cmd/fscrypt/flags.go +++ b/cmd/fscrypt/flags.go @@ -29,25 +29,26 @@ import ( "time" "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/cmd" "github.com/google/fscrypt/security" "github.com/google/fscrypt/util" ) // Bool flags: used to switch some behavior on or off var ( - legacyFlag = &boolFlag{ + legacyFlag = &cmd.BoolFlag{ Name: "legacy", Usage: `Allow for support of older kernels with ext4 (before v4.8) and F2FS (before v4.6) filesystems.`, Default: true, } - skipUnlockFlag = &boolFlag{ + skipUnlockFlag = &cmd.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.`, } - 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 @@ -59,7 +60,7 @@ var ( // Option flags: used to specify options instead of being prompted for them var ( - timeTargetFlag = &durationFlag{ + timeTargetFlag = &cmd.DurationFlag{ Name: "time", ArgName: "TIME", Usage: `Set the global options so that passphrase hashing takes @@ -69,7 +70,7 @@ var ( units are "ms", "s", "m", and "h".`, Default: 1 * time.Second, } - sourceFlag = &stringFlag{ + sourceFlag = &cmd.StringFlag{ Name: "source", ArgName: "SOURCE", Usage: fmt.Sprintf(`New protectors will have type SOURCE. SOURCE @@ -78,14 +79,14 @@ var ( the source, with a default pulled from %s.`, actions.ConfigFileLocation), } - nameFlag = &stringFlag{ + nameFlag = &cmd.StringFlag{ Name: "name", ArgName: "PROTECTOR_NAME", Usage: `New custom_passphrase and raw_key protectors will be named PROTECTOR_NAME. If not specified, the user will be prompted for a name.`, } - keyFileFlag = &stringFlag{ + keyFileFlag = &cmd.StringFlag{ Name: "key", ArgName: "FILE", Usage: `Use the contents of FILE as the wrapping key when @@ -93,20 +94,20 @@ var ( formatted as raw binary and should be exactly 32 bytes long.`, } - userFlag = &stringFlag{ + userFlag = &cmd.StringFlag{ Name: "user", ArgName: "USERNAME", Usage: `Specifiy which user should be used for login passphrases or to which user's keyring keys should be provisioned.`, } - protectorFlag = &stringFlag{ + protectorFlag = &cmd.StringFlag{ Name: "protector", ArgName: "MOUNTPOINT:ID", Usage: `Specify an existing protector on filesystem MOUNTPOINT with protector descriptor ID which should be used in the command.`, } - unlockWithFlag = &stringFlag{ + unlockWithFlag = &cmd.StringFlag{ Name: "unlock-with", ArgName: "MOUNTPOINT:ID", Usage: `Specify an existing protector on filesystem MOUNTPOINT @@ -116,7 +117,7 @@ var ( multiple protectors. If not specified, the user will be prompted for a protector.`, } - policyFlag = &stringFlag{ + policyFlag = &cmd.StringFlag{ Name: "policy", ArgName: "MOUNTPOINT:ID", Usage: `Specify an existing policy on filesystem MOUNTPOINT with diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go index f1649fc..65da7cb 100644 --- a/cmd/fscrypt/fscrypt.go +++ b/cmd/fscrypt/fscrypt.go @@ -27,9 +27,35 @@ package main import ( "fmt" + "github.com/google/fscrypt/filesystem" + "github.com/google/fscrypt/security" + "github.com/pkg/errors" + + "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/cmd" ) +// Arguments used in fscrypt commands. +var ( + unusedMountpointArg = &cmd.Argument{ + Name: "mountpoint", + Usage: "path to a mountpoint on which to setup fscrypt", + } + usedMountpointArg = &cmd.Argument{ + Name: "mountpoint", + Usage: "path to a mountpoint being used with fscrypt", + } + directoryToEncryptArg = &cmd.Argument{ + Name: "directory", + Usage: "path to an empty directory to encrypt with fscrypt", + } + encryptedPathArg = &cmd.Argument{ + Name: "path", + Usage: "file or directory encrypted with fscrypt", + } +) + func main() { fscryptCommand.Run() } var fscryptCommand = cmd.Command{ @@ -40,10 +66,10 @@ var fscryptCommand = cmd.Command{ cmd.VersionUsage, }, SubCommands: []*Command{ - &setupCommand, - &encryptCommand, - // unlockCommand, - // purgeCommand, + setupCommand, + encryptCommand, + unlockCommand, + purgeCommand, // statusCommand, // metadataCommand, cmd.VersionCommand, @@ -58,9 +84,9 @@ var setupCommand = &cmd.Command{ Title: "setup a system/filesystem to use fscrypt", UsageLines: []string{ fmt.Sprintf("[options]"), - fmt.Sprintf("%s [%s]", mountpointArg, cmd.ForceFlag), + fmt.Sprintf("%s [%s]", unusedMountpointArg, cmd.ForceFlag), }, - Arguments: []*cmd.Argument{mountpointArg}, + Arguments: []*cmd.Argument{unusedMountpointArg}, InheritFlags: true, Flags: []cmd.Flag{configFileFlag, targetFlag, legacyFlag, cmd.ForceFlag}, ManPage: &cmd.ManPage{Name: "fscrypt-setup", Section: 8}, @@ -80,5 +106,151 @@ func setupAction(c *cmd.Context) error { } } -// encrypt performs the functions of setupDirectory and Unlock in one command. -var encryptCommand = &cmd.Command{} +// encrypt takes an empty directory, enables encryption, and unlocks it. +var encryptCommand = &cmd.Command{ + Name: "encrypt", + Title: "start encrypting an empty directory", + UsageLines: nil, // TODO(joerichey) + Arguments: []*cmd.Argument{directoryToEncryptArg}, + InheritFlags: true, + Flags: []cmd.Flag{sourceFlag, nameFlag, protectorFlag, policyFlag, + keyFileFlag, userFlag, skipUnlockFlag}, + ManPage: &cmd.ManPage{Name: "fscrypt-encrypt", Section: 8}, + Action: encryptAction, +} + +func encryptAction(c *cmd.Context) error { + if err := cmd.CheckExpectedArgs(c, 1, false); err != nil { + return err + } + + path := c.Args[0] + if err := encryptPath(path); err != nil { + return err + } + + if !skipUnlockFlag.Value { + fmt.Fprintf(cmd.Output, "%q is now encrypted, unlocked, and ready for use.\n", path) + return nil + } + + fmt.Fprintf(cmd.Output, "%q is now encrypted, but it is still locked.\n", path) + fmt.Fprintf(cmd.Output, "It can be unlocked with: fscrypt unlock %q\n", path) + return nil +} + +// unlock takes an encrypted path and makes it available for reading/writing. +var unlockCommand = &cmd.Command{ + Name: "unlock", + Title: "unlock an encrypted file or directory", + UsageLines: nil, // TODO(joerichey) + Arguments: []*cmd.Argument{encryptedPathArg}, + InheritFlags: true, + Flags: []cmd.Flag{protectorFlag, policyFlag, keyFileFlag, userFlag}, + ManPage: &cmd.ManPage{Name: "fscrypt-unlock", Section: 8}, + Action: unlockAction, +} + +func unlockAction(c *cmd.Context) error { + if err := cmd.CheckExpectedArgs(c, 1, false); err != nil { + return err + } + + path := c.Args[0] + if err := unlockPath(path); err != nil { + return err + } + + fmt.Fprintf(cmd.Output, "%q is now unlocked and ready for use.\n", path) + return nil +} + +// purge removes all the policy keys from the keyring (my require unmount). +var purgeCommand = &cmd.Command{ + Name: "purge", + Title: "remove a directory's encryption keys", + UsageLines: []string{fmt.Sprintf("%s, [%s=false] [%s] [%s]", + usedMountpointArg, dropCachesFlag, userFlag, cmd.ForceFlag)}, + Arguments: []*cmd.Argument{usedMountpointArg}, + InheritFlags: true, + Flags: []cmd.Flag{dropCachesFlag, userFlag, cmd.ForceFlag}, + ManPage: &cmd.ManPage{Name: "fscrypt-purge", Section: 8}, + Action: purgeAction, +} + +func purgeAction(c *cmd.Context) error { + if err := cmd.CheckExpectedArgs(c, 1, false); err != nil { + return err + } + if dropCachesFlag.Value { + if cmd.CheckIfRoot() != nil { + return ErrDropCachesPerm + } + } + + targetUser, err := parseUserFlag(true) + if err != nil { + return err + } + ctx, err := actions.NewContextFromMountpoint(c.Args[0], target) + if err != nil { + return 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 = cmd.AskConfirmation(question+"?", warning, false); err != nil { + return err + } + if err = actions.PurgeAllPolicies(ctx); err != nil { + return err + } + fmt.Fprintf(cmd.Output, "Policies purged from filesystem %q.\n", ctx.Mount.Path) + + if !dropCachesFlag.Value { + fmt.Fprintf(cmd.Output, "Filesystem %q should now be unmounted.\n", cmd.Mount.Path) + return nil + } + if err = security.DropFilesystemCache(); err != nil { + return err + } + fmt.Fprintln(cmd.Output, "Encrypted data removed from filesystem cache.") + return nil +} + +// status is a command that gets info about the system, a mountpoint, or a path. +var statusCommand = &cmd.Command{ + Name: "status", + Title: "get the status of the system or a path", + UsageLines: []string{"", usedMountpointArg.String(), encryptedPathArg.String()}, + Flags: []cmd.Flag{cmd.VerboseFlag, cmd.HelpFlag}, + ManPage: &cmd.ManPage{Name: "fscrypt-status", Section: 8}, + Action: statusAction, +} + +func statusAction(c *cmd.Context) error { + switch len(c.Args) { + case 0: + // Case (1) - global status + return writeGlobalStatus() + case 1: + path := c.Args[0] + ctx, mntErr := actions.NewContextFromMountpoint(path, nil) + + switch errors.Cause(mntErr) { + case nil: + // Case (2) - mountpoint status + return writeFilesystemStatus(ctx) + case filesystem.ErrNotAMountpoint: + // Case (3) - file or directory status + return writePathStatus(path) + default: + return mntErr + } + default: + return expectedArgsErr(c, 1, true) + } +} diff --git a/cmd/fscrypt/prompt.go b/cmd/fscrypt/prompt.go index bccf534..57d0fc7 100644 --- a/cmd/fscrypt/prompt.go +++ b/cmd/fscrypt/prompt.go @@ -23,22 +23,13 @@ import ( "fmt" "log" "os" - "os/user" "strconv" - "github.com/pkg/errors" - "github.com/google/fscrypt/actions" "github.com/google/fscrypt/metadata" "github.com/google/fscrypt/util" ) -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", @@ -46,26 +37,6 @@ var sourceDescriptions = map[metadata.SourceType]string{ metadata.SourceType_raw_key: "A raw 256-bit key", } -// usernameFromID returns the username for the provided UID. If the UID does not -// correspond to a user or the username is blank, an error is returned. -func usernameFromID(uid int64) (string, error) { - u, err := user.LookupId(strconv.Itoa(int(uid))) - if err != nil || u.Username == "" { - return "", errors.Wrapf(ErrUnknownUser, "uid %d", uid) - } - return u.Username, nil -} - -// formatUsername either returns the username for the provided UID, or a string -// containing the error for unknown UIDs. -func formatUsername(uid int64) string { - username, err := usernameFromID(uid) - if err != nil { - return fmt.Sprintf("[%v]", err) - } - return username -} - // formatInfo gives a string description of metadata.ProtectorData. func formatInfo(data actions.ProtectorInfo) string { switch data.Source() { diff --git a/cmd/fscrypt/strings.go b/cmd/fscrypt/strings.go deleted file mode 100644 index 07b6b64..0000000 --- a/cmd/fscrypt/strings.go +++ /dev/null @@ -1,52 +0,0 @@ -/* - * strings.go - File which contains the specific strings used for output and - * formatting in fscrypt. - * - * 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" -) - -// Argument usage strings -const ( - directoryArg = "DIRECTORY" - mountpointArg = "MOUNTPOINT" - pathArg = "PATH" - mountpointIDArg = mountpointArg + ":ID" -) - -// Add words to this map if pluralization does not just involve adding an s. -var plurals = map[string]string{ - "policy": "policies", -} - -// pluralize prints our 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 { - if plural, ok := plurals[word]; ok { - word = plural - } else { - word += "s" - } - } - return fmt.Sprintf("%d %s", count, word) -} diff --git a/cmd/run.go b/cmd/run.go index a23de2d..7765303 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -63,7 +63,7 @@ Commands: Arguments: {{- range $arguments}} {{.}} - {{wrapText .Usage 2 -}} + {{WrapText .Usage 2 -}} {{end}} {{end -}} @@ -71,7 +71,7 @@ Arguments: Options: {{- range $flags}} {{.}} - {{wrapText .FullUsage 2 -}} + {{WrapText .FullUsage 2 -}} {{end}} {{end -}} -- cgit v1.2.3