diff options
| -rw-r--r-- | cmd/fscrypt/commands.go | 296 | ||||
| -rw-r--r-- | cmd/fscrypt/errors.go | 188 | ||||
| -rw-r--r-- | cmd/fscrypt/flags.go | 261 | ||||
| -rw-r--r-- | cmd/fscrypt/format.go | 162 | ||||
| -rw-r--r-- | cmd/fscrypt/fscrypt.go | 117 | ||||
| -rw-r--r-- | cmd/fscrypt/fscrypt_test.go | 4 | ||||
| -rw-r--r-- | cmd/fscrypt/keys.go | 198 | ||||
| -rw-r--r-- | cmd/fscrypt/prompt.go | 322 | ||||
| -rw-r--r-- | cmd/fscrypt/protector.go | 123 | ||||
| -rw-r--r-- | cmd/fscrypt/setup.go | 77 | ||||
| -rw-r--r-- | cmd/fscrypt/strings.go | 143 |
11 files changed, 1884 insertions, 7 deletions
diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go new file mode 100644 index 0000000..2407f32 --- /dev/null +++ b/cmd/fscrypt/commands.go @@ -0,0 +1,296 @@ +/* + * commands.go - Implementations of all of the fscrypt commands and subcommands. + * This mostly just calls into the fscrypt/actions package. + * + * 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" + "log" + "os" + + "github.com/pkg/errors" + "github.com/urfave/cli" + + "fscrypt/actions" + "fscrypt/metadata" +) + +// 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, + 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 +} + +// 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 +// metadata creation is reverted, and the directory is unmodified. +func encryptPath(path string) (err error) { + ctx, err := actions.NewContextFromPath(path) + if err != nil { + return + } + if err = checkEncryptable(ctx, path); err != nil { + return + } + + var policy *actions.Policy + if policyFlag.Value != "" { + log.Printf("getting policy for %q", path) + + policy, err = getPolicyFromFlag(policyFlag.Value) + } else { + log.Printf("creating policy for %q", path) + + var protector *actions.Protector + protector, err = selectOrCreateProtector(ctx) + // Successfully created protector should be reverted on failure. + if err != nil { + return + } + defer func() { + protector.Lock() + if err != nil { + protector.Revert() + } + }() + + if err = protector.Unlock(existingKeyFn); err != nil { + return + } + policy, err = actions.CreatePolicy(ctx, protector) + } + // Successfully created policy should be reverted on failure. + if err != nil { + return + } + defer func() { + policy.Lock() + policy.Deprovision() + if err != nil { + policy.Revert() + } + }() + + // Unlock() first, so if the Unlock() fails the directory isn't changed. + if !skipUnlockFlag.Value { + if err = policy.Unlock(optionFn, existingKeyFn); err != nil { + return + } + if err = policy.Provision(); err != nil { + return + } + } + if err = policy.Apply(path); os.IsPermission(errors.Cause(err)) { + // EACCES at this point indicates ownership issues. + err = errors.Wrap(ErrBadOwners, path) + } + return +} + +// checkEncryptable returns an error if the path cannot be encrypted. +func checkEncryptable(ctx *actions.Context, path string) error { + log.Printf("ensuring %s is an empty and readable directory", path) + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + switch names, err := f.Readdirnames(-1); { + case err != nil: + // Could not read directory (might not be a directory) + log.Print(errors.Wrap(err, path)) + return errors.Wrap(ErrNotEmptyDir, path) + case len(names) > 0: + log.Printf("directory %s is not empty", path) + return errors.Wrap(ErrNotEmptyDir, path) + } + + log.Printf("ensuring %s is not encrypted and filesystem is using fscrypt", path) + switch _, err := actions.GetPolicyFromPath(ctx, path); errors.Cause(err) { + case metadata.ErrNotEncrypted: + // We are not encrypted + return nil + case nil: + // We are encrypted + return errors.Wrap(metadata.ErrEncrypted, path) + default: + return err + } +} + +// selectOrCreateProtector uses user input (or flags) to either create a new +// protector or select and existing one. The created return value is true if we +// created a new protector. +func selectOrCreateProtector(ctx *actions.Context) (*actions.Protector, error) { + if protectorFlag.Value != "" { + return getProtectorFromFlag(protectorFlag.Value) + } + + options, err := expandedProtectorOptions(ctx) + if err != nil { + return nil, err + } + + // Having no existing options to choose from or using creation-only + // flags indicates we should make a new protector. + if len(options) == 0 || nameFlag.Value != "" || sourceFlag.Value != "" { + return createProtectorFromContext(ctx) + } + + created, err := askQuestion("Should we create a new protector?", false) + if err != nil { + return nil, err + } + if created { + return createProtectorFromContext(ctx) + } + + log.Print("finding an existing protector to use") + return selectExistingProtector(ctx, options) +} + +// 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}, + Action: unlockAction, +} + +func unlockAction(c *cli.Context) error { + if c.NArg() != 1 { + return expectedArgsErr(c, 1, false) + } + + path := c.Args().Get(0) + ctx, err := actions.NewContextFromPath(path) + if err != nil { + return newExitError(c, 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) + } + // Check if directory is already unlocked + if policy.IsProvisioned() { + log.Printf("policy %s is already provisioned", policy) + return newExitError(c, 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 +} diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go new file mode 100644 index 0000000..aa2f2ab --- /dev/null +++ b/cmd/fscrypt/errors.go @@ -0,0 +1,188 @@ +/* + * errors.go - File which contains common error handling code for fscrypt + * commands. This includes handling for bad usage, invalid commands, and errors + * from the other packages + * + * 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 ( + "bytes" + "fmt" + "os" + "path/filepath" + "unicode/utf8" + + "github.com/pkg/errors" + "github.com/urfave/cli" + + "fscrypt/actions" + "fscrypt/filesystem" + "fscrypt/metadata" + "fscrypt/util" +) + +// failureExitCode is the value fscrypt will return on failure. +const failureExitCode = 1 + +// Various errors used for the top level user interface +var ( + ErrReadingStdin = util.SystemError("read from standard input failed") + ErrCanceled = errors.New("operation canceled") + ErrNoDesctructiveOps = errors.New("operation would be destructive") + ErrMaxPassphrase = util.SystemError("max passphrase length exceeded") + ErrPAMPassphrase = errors.New("incorrect login passphrase") + ErrInvalidSource = errors.New("invalid source type") + ErrPassphraseMismatch = errors.New("entered passphrases do not match") + ErrSpecifyProtector = errors.New("multiple protectors available") + ErrWrongKey = errors.New("incorrect key provided") + ErrSpecifyKeyFile = errors.New("no key file specified") + ErrKeyFileLength = errors.Errorf("key file must be %d bytes", metadata.PolicyKeyLen) + 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") + ErrNotPassphrase = errors.New("protector does not use a passphrase") +) + +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 +} + +// getErrorSuggestions returns a string containing suggestions about how to fix +// an error. If no suggestion is necessary or available, return empty 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) + case metadata.ErrEncryptionNotSupported: + return `Encryption for this type of filesystem is not supported + on this kernel version.` + case metadata.ErrEncryptionNotEnabled: + return `Encryption is either disabled in the kernel config, or + needs to be enabled for this filesystem. See the + documentation on how to enable encryption on ext4 + systems (and the risks of doing so).` + 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: + return `This file or directory has either been encrypted with + another tool (such as e4crypt) or the corresponding + filesystem metadata has been deleted.` + case actions.ErrPolicyMetadataMismatch: + return `The metadata for this encrypted directory is in an + 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)) + case ErrSpecifyProtector: + return fmt.Sprintf("Use %s to specify a protector.", shortDisplay(protectorFlag)) + case ErrSpecifyKeyFile: + return fmt.Sprintf("Use %s to specify a key file.", shortDisplay(keyFileFlag)) + case ErrBadOwners: + return `Encryption can only be setup on directories you own, + even if you have write permission for the directory.` + case ErrNotEmptyDir: + return `Encryption can only be setup on empty directories; files + cannot be encrypted in-place. Instead, encrypt an empty + directory, copy the files into that encrypted directory, + and securely delete the originals with "shred".` + 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()} +} diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go new file mode 100644 index 0000000..da3116f --- /dev/null +++ b/cmd/fscrypt/flags.go @@ -0,0 +1,261 @@ +/* + * flags.go - File which contains all the flags used by the application. This + * includes both global flags and command specific flags. When applicable, it + * also includes the default values. + * + * 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 ( + "flag" + "fmt" + "fscrypt/actions" + "log" + "regexp" + "strconv" + "time" + + "github.com/urfave/cli" +) + +// 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 == false { + 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.`, + 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.`, + } +) + +// Option flags: used to specify options instead of being prompted for them +var ( + timeTargetFlag = &durationFlag{ + Name: "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".`, + Default: 1 * time.Second, + } + sourceFlag = &stringFlag{ + Name: "source", + ArgName: "SOURCE", + Usage: fmt.Sprintf(`New protectors will have type SOURCE. SOURCE + can be one of pam_passphrase, custom_passphrase, or + raw_key. If not specified, the user will be prompted for + the source, with a default pulled from %s.`, + actions.ConfigFileLocation), + } + nameFlag = &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{ + Name: "key", + ArgName: "FILE", + Usage: `Use the contents of FILE as the wrapping key when + creating or unlocking raw_key protectors. FILE should be + formatted as raw binary and should be exactly 32 bytes + long.`, + } + protectorFlag = &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{ + Name: "unlock-with", + ArgName: "MOUNTPOINT:ID", + Usage: `Specify an existing protector on filesystem MOUNTPOINT + with protector descriptor ID which should be used to + unlock a policy (usually specified with --policy). This + flag is only useful if a policy is protected with + multiple protectors. If not specified, the user will be + prompted for a protector.`, + } + policyFlag = &stringFlag{ + Name: "policy", + ArgName: "MOUNTPOINT:ID", + Usage: `Specify an existing policy on filesystem MOUNTPOINT with + key descriptor ID which should be used in the command.`, + } +) + +// The first group is optional and corresponds to the mountpoint. The second +// group is required and corresponds to the descriptor. +var idFlagRegex = regexp.MustCompile("^([[:print:]]+):([[:alnum:]]+)$") + +// parseMetadataFlag takes the value of either protectorFlag or policyFlag +// formatted as MOUNTPOINT:DESCRIPTOR, and returns a context for the mountpoint +// and a string for the descriptor. +func parseMetadataFlag(flagValue string) (*actions.Context, string, error) { + matches := idFlagRegex.FindStringSubmatch(flagValue) + if matches == nil { + err := fmt.Errorf("flag value %q does not have format %s", flagValue, mountpointIDArg) + return nil, "", err + } + + mountpoint := matches[1] + descriptor := matches[2] + log.Printf("parsed flag: mountpoint=%q descriptor=%s", mountpoint, descriptor) + + ctx, err := actions.NewContextFromMountpoint(mountpoint) + return ctx, descriptor, err +} + +// getProtectorFromFlag gets an existing locked protector from protectorFlag. +func getProtectorFromFlag(flagValue string) (*actions.Protector, error) { + ctx, descriptor, err := parseMetadataFlag(flagValue) + if err != nil { + return nil, err + } + return actions.GetProtector(ctx, descriptor) +} + +// getPolicyFromFlag gets an existing locked policy from policyFlag. +func getPolicyFromFlag(flagValue string) (*actions.Policy, error) { + ctx, descriptor, err := parseMetadataFlag(flagValue) + if err != nil { + return nil, err + } + return actions.GetPolicy(ctx, descriptor) +} diff --git a/cmd/fscrypt/format.go b/cmd/fscrypt/format.go new file mode 100644 index 0000000..39cc71f --- /dev/null +++ b/cmd/fscrypt/format.go @@ -0,0 +1,162 @@ +/* + * format.go - Contains all the functionality for formatting the command line + * output. This includes formatting the description and flags so that the whole + * text is <= LineLength characters. + * + * 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 ( + "bytes" + "fmt" + "os" + "regexp" + "strings" + "unicode/utf8" + + "github.com/urfave/cli" + "golang.org/x/crypto/ssh/terminal" + + "fscrypt/util" +) + +var ( + // lineLength is the maximum width of fscrypt's formatted output. It is + // usually the width of the terminal. + lineLength int + fallbackLineLength = 80 // fallback is punch cards + maxLineLength = 120 + // IndentLength is the number spaces to indent by. + indentLength = 2 + // length of the longest shortDisplay for a flag + maxShortDisplay int + // how much the a flag's usage text needs to be moved over + flagPaddingLength int +) + +// We use the init() function to compute our longest short display length. This +// is then used to compute the formatting and padding strings. This ensures we +// will always have room to display our flags, and the flag descriptions always +// appear in the same place. +func init() { + for _, flag := range allFlags { + displayLength := utf8.RuneCountInString(shortDisplay(flag)) + if displayLength > maxShortDisplay { + maxShortDisplay = displayLength + } + } + + // Pad usage enough so the flags have room. + flagPaddingLength = maxShortDisplay + 2*indentLength + + // We use the width of the terminal unless we cannot get the width. + width, _, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + lineLength = fallbackLineLength + } else { + lineLength = util.MinInt(width, maxLineLength) + } + +} + +// Flags that conform to this interface can be used with an urfave/cli +// application and can be printed in the correct format. +type prettyFlag interface { + cli.Flag + GetArgName() string + GetUsage() string +} + +// How a flag should appear on the command line. We have two formats: +// --name +// --name=ARG_NAME +// The ARG_NAME appears if the prettyFlag's GetArgName() method returns a +// non-empty string. The returned string from shortDisplay() does not include +// any leading or trailing whitespace. +func shortDisplay(f prettyFlag) string { + if argName := f.GetArgName(); argName != "" { + return fmt.Sprintf("--%s=%s", f.GetName(), argName) + } + return fmt.Sprintf("--%s", f.GetName()) +} + +// How our flags should appear when displaying their usage. An example would be: +// +// --help Prints help screen for commands and subcommands. +// +// If a default is specified, this if appended to the usage. Example: +// +// --legacy Allow for support of older kernels with ext4 +// (before v4.8) and F2FS (before v4.6) filesystems. +// (default: true) +// +func longDisplay(f prettyFlag, defaultString ...string) string { + usage := f.GetUsage() + if len(defaultString) > 0 { + usage += fmt.Sprintf(" (default: %v)", defaultString[0]) + } + + // We pad the the shortDisplay on the right with enough spaces to equal + // the longest flag's display + shortDisp := shortDisplay(f) + length := utf8.RuneCountInString(shortDisp) + shortDisp += strings.Repeat(" ", maxShortDisplay-length) + + return indent + shortDisp + indent + wrapText(usage, flagPaddingLength) +} + +// Regex that determines if we are starting an ordered list +var listRegex = regexp.MustCompile(`^\([\d]+\)$`) + +// Takes an input string text, and wraps the text so that each line begins with +// padding spaces (except for the first line), 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, padding int) string { + // We use a buffer to format the wrapped text so we get O(n) runtime + var buffer bytes.Buffer + spaceLeft := 0 + maxTextLen := lineLength - padding + delimiter := strings.Repeat(" ", padding) + for i, word := range strings.Fields(text) { + wordLen := utf8.RuneCountInString(word) + switch { + case i == 0: + // No delimiter for the first line + buffer.WriteString(word) + spaceLeft = maxTextLen - wordLen + case listRegex.Match([]byte(word)): + // Add an additional line to separate list items. + buffer.WriteString("\n") + fallthrough + case wordLen+1 > spaceLeft: + // If no room left, write the word on the next line. + buffer.WriteString("\n") + buffer.WriteString(delimiter) + buffer.WriteString(word) + spaceLeft = maxTextLen - wordLen + default: + // Write word on this line + buffer.WriteByte(' ') + buffer.WriteString(word) + spaceLeft -= 1 + wordLen + } + } + + return buffer.String() +} diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go index 191d4fb..d3185fa 100644 --- a/cmd/fscrypt/fscrypt.go +++ b/cmd/fscrypt/fscrypt.go @@ -1,5 +1,6 @@ /* - * fscrypt.go - Stub file which currently just prints out the time + * fscrypt.go - File which starts up and runs the application. Initializes + * information about the application like the name, version, author, etc... * * Copyright 2017 Google Inc. * Author: Joe Richey (joerichey@google.com) @@ -19,16 +20,124 @@ /* fscrypt is a comprehensive command line tool for managing filesystem encryption. - -It currently just tells the time. */ package main import ( "fmt" + "io/ioutil" + "log" + "os" "time" + + "github.com/urfave/cli" +) + +var ( + // Current version of the program (set by Makefile) + version string + // Formatted build time (set by Makefile) + buildTime string + // Authors to display in the info command + Authors = []cli.Author{{ + Name: "Joe Richey", + Email: "joerichey@google.com", + }} ) func main() { - fmt.Printf("The time is now: %v\n", time.Now()) + cli.AppHelpTemplate = appHelpTemplate + cli.CommandHelpTemplate = commandHelpTemplate + cli.SubcommandHelpTemplate = subcommandHelpTemplate + + // Create our command line application + app := cli.NewApp() + app.Usage = shortUsage + app.Authors = Authors + app.Copyright = apache2GoogleCopyright + + // Grab the version and compilation time passed in from the Makefile. + app.Version = version + app.Compiled, _ = time.Parse(time.UnixDate, buildTime) + app.OnUsageError = onUsageError + + // Setup global flags + cli.HelpFlag = helpFlag + cli.VersionFlag = versionFlag + cli.VersionPrinter = func(c *cli.Context) { + cli.HelpPrinter(c.App.Writer, versionInfoTemplate, c.App) + } + app.Flags = universalFlags + + // We hide the help subcommand so that "fscrypt <command> --help" works + // and "fscrypt <command> help" does not. + app.HideHelp = true + + // Initialize command list and setup all of the commands. + app.Action = defaultAction + app.Commands = []cli.Command{Setup, Encrypt, Unlock} + for i := range app.Commands { + setupCommand(&app.Commands[i]) + } + + app.Run(os.Args) +} + +// setupCommand performs some common setup for each command. This includes +// hiding the help, formating the description, adding in the necessary +// flags, setting up error handlers, etc... Note that the command is modified +// in place and its subcommands are also setup. +func setupCommand(command *cli.Command) { + command.Description = wrapText(command.Description, indentLength) + command.HideHelp = true + command.Flags = append(command.Flags, universalFlags...) + + if command.Action == nil { + command.Action = defaultAction + } + + // Setup function handlers + command.OnUsageError = onUsageError + if len(command.Subcommands) == 0 { + command.Before = setupOutputs + } else { + // Cleanup subcommands (if applicable) + for i := range command.Subcommands { + setupCommand(&command.Subcommands[i]) + } + } +} + +// setupOutputs makes sure our logs, errors, and output are going to the correct +// io.Writers and that we haven't over-specified our flags. We only print the +// logs when using verbose, and only print normal stuff when not using quiet. +func setupOutputs(c *cli.Context) error { + log.SetOutput(ioutil.Discard) + c.App.Writer = ioutil.Discard + + if verboseFlag.Value { + log.SetOutput(os.Stdout) + } + if !quietFlag.Value { + c.App.Writer = os.Stdout + } + return nil +} + +// defaultAction will be run when no command is specified. +func defaultAction(c *cli.Context) error { + // Always default to showing the help + if helpFlag.Value { + cli.ShowAppHelp(c) + return nil + } + + // Only exit when not calling with the help command + var message string + if args := c.Args(); args.Present() { + message = fmt.Sprintf("command \"%s\" not found", args.First()) + } else { + message = "no command was specified" + } + return &usageError{c, message} } diff --git a/cmd/fscrypt/fscrypt_test.go b/cmd/fscrypt/fscrypt_test.go index cbeacdf..1d09bf8 100644 --- a/cmd/fscrypt/fscrypt_test.go +++ b/cmd/fscrypt/fscrypt_test.go @@ -19,8 +19,6 @@ package main -import ( - "testing" -) +import "testing" func TestTrivial(t *testing.T) {} diff --git a/cmd/fscrypt/keys.go b/cmd/fscrypt/keys.go new file mode 100644 index 0000000..45dc294 --- /dev/null +++ b/cmd/fscrypt/keys.go @@ -0,0 +1,198 @@ +/* + * keys.go - Functions and readers for getting passphrases and raw keys via + * the command line. Includes ability to hide the entered passphrase, or use a + * raw key as input. + * + * 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" + "io" + "log" + "os" + + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/terminal" + + "fscrypt/actions" + "fscrypt/crypto" + "fscrypt/metadata" + "fscrypt/pam" +) + +// The file descriptor for standard input +const stdinFd = 0 + +// actions.KeyFuncs for getting or creating cryptographic keys +var ( + // getting an existing key + existingKeyFn = makeKeyFunc(true, false, "") + // creating a new key + createKeyFn = makeKeyFunc(false, true, "") +) + +// passphraseReader is an io.Reader intended for terminal passphrase input. The +// struct is empty as the reader needs to maintain no internal state. +type passphraseReader struct{} + +// Read gets input from the terminal until a newline is encountered. This read +// should be called with the maximum buffer size for the passphrase. +func (p passphraseReader) Read(buf []byte) (int, error) { + // We read one byte at a time to handle backspaces + position := 0 + for { + if position == len(buf) { + return position, ErrMaxPassphrase + } + if _, err := io.ReadFull(os.Stdin, buf[position:position+1]); err != nil { + return position, err + } + switch buf[position] { + case '\r', '\n': + return position, io.EOF + case 3, 4: + return position, ErrCanceled + case 8, 127: + if position > 0 { + position-- + } + default: + position++ + } + } +} + +// getPassphraseKey puts the terminal into raw mode for the entry of the user's +// passphrase into a key. If we are not reading from a terminal, just read into +// the passphrase into the key normally. +func getPassphraseKey(prompt string) (*crypto.Key, error) { + if !quietFlag.Value { + fmt.Printf(prompt) + } + + // Only disable echo if stdin is actually a terminal. + if terminal.IsTerminal(stdinFd) { + state, err := terminal.MakeRaw(stdinFd) + if err != nil { + return nil, err + } + defer func() { + terminal.Restore(stdinFd, state) + fmt.Println() // To align input + }() + } + + return crypto.NewKeyFromReader(passphraseReader{}) +} + +// makeKeyFunc creates an actions.KeyFunc. This function customizes the KeyFunc +// to whether or not it supports retrying, whether it confirms the passphrase, +// and custom prefix for printing (if any). +func makeKeyFunc(supportRetry, shouldConfirm bool, prefix string) actions.KeyFunc { + return func(info actions.ProtectorInfo, retry bool) (*crypto.Key, error) { + log.Printf("KeyFunc(%s, %v)", formatInfo(info), retry) + if retry { + if !supportRetry { + panic("this KeyFunc does not support retrying") + } + // Don't retry for non-interactive sessions + if quietFlag.Value { + return nil, ErrWrongKey + } + fmt.Println("Incorrect Passphrase") + } + + switch info.Source() { + case metadata.SourceType_pam_passphrase: + prompt := fmt.Sprintf("Enter %slogin passphrase for %s: ", + prefix, getUsername(info.UID())) + key, err := getPassphraseKey(prompt) + if err != nil { + return nil, err + } + + // To confirm, check that the passphrase is the user's + // login passphrase. + if shouldConfirm { + username := getUsername(info.UID()) + ok, err := pam.IsUserLoginToken(username, key) + if err != nil { + key.Wipe() + return nil, err + } + if !ok { + key.Wipe() + return nil, ErrPAMPassphrase + } + } + return key, nil + + case metadata.SourceType_custom_passphrase: + prompt := fmt.Sprintf("Enter %scustom passphrase for protector %q: ", + prefix, info.Name()) + key, err := getPassphraseKey(prompt) + if err != nil { + return nil, err + } + + // To confirm, make sure the user types the same + // passphrase in again. + if shouldConfirm && !quietFlag.Value { + key2, err := getPassphraseKey("Confirm passphrase: ") + if err != nil { + key.Wipe() + return nil, err + } + defer key2.Wipe() + + if !key.Equals(key2) { + key.Wipe() + return nil, ErrPassphraseMismatch + } + } + return key, nil + + case metadata.SourceType_raw_key: + // Only use prefixes with passphrase protectors. + if prefix != "" { + return nil, ErrNotPassphrase + } + prompt := fmt.Sprintf("Enter key file for protector %q: ", info.Name()) + // Raw keys use a file containing the key data. + file, err := promptForKeyFile(prompt) + if err != nil { + return nil, err + } + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + + if fileInfo.Size() != metadata.InternalKeyLen { + return nil, errors.Wrap(ErrKeyFileLength, file.Name()) + } + return crypto.NewFixedLengthKeyFromReader(file, metadata.InternalKeyLen) + + default: + return nil, ErrInvalidSource + } + } +} diff --git a/cmd/fscrypt/prompt.go b/cmd/fscrypt/prompt.go new file mode 100644 index 0000000..56dcf06 --- /dev/null +++ b/cmd/fscrypt/prompt.go @@ -0,0 +1,322 @@ +/* + * prompt.go - Functions for handling user input and options + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "os/user" + "strconv" + "strings" + + "fscrypt/actions" + "fscrypt/metadata" +) + +const ( + // Suffixes for questions with a yes or no default + defaultYesSuffix = " [Y/n] " + defaultNoSuffix = " [y/N] " +) + +// Descriptions for each of the protector sources +var sourceDescriptions = map[metadata.SourceType]string{ + metadata.SourceType_pam_passphrase: "Your login passphrase", + metadata.SourceType_custom_passphrase: "A custom passphrase", + metadata.SourceType_raw_key: "A raw 256-bit key", +} + +// promptUser presents a message to the user and returns their input string. An +// error is returned if our read from standard input fails. +func promptUser(prompt string) (string, error) { + scanner := bufio.NewScanner(os.Stdin) + fmt.Print(prompt) + if !scanner.Scan() { + return "", ErrReadingStdin + } + return scanner.Text(), nil +} + +// askQuestion asks the user a yes or no question. Returning a boolean on a +// successful answer and an error if there was not a response from the user. +// Returns the defaultChoice on empty input (or in quiet mode). +func askQuestion(question string, defaultChoice bool) (bool, error) { + // If in quiet mode, we just use the default + if quietFlag.Value { + return defaultChoice, nil + } + // Loop until failure or valid input + var input string + var err error + for { + if defaultChoice { + input, err = promptUser(question + defaultYesSuffix) + } else { + input, err = promptUser(question + defaultNoSuffix) + } + if err != nil { + return false, err + } + + switch strings.ToLower(input) { + case "y", "yes": + return true, nil + case "n", "no": + return false, nil + case "": + return defaultChoice, nil + } + } +} + +// askConfirmation asks the user for confirmation of a specific action. An error +// is returned if the user declines or IO fails. +func askConfirmation(question string, defaultChoice bool, warning string) error { + // All confirmations are "yes" if we are forcing. + if forceFlag.Value { + return nil + } + + // Defaults of "no" require forcing. + if !defaultChoice { + if quietFlag.Value { + return ErrNoDesctructiveOps + } + } + + if warning != "" && !quietFlag.Value { + fmt.Println(wrapText("WARNING: "+warning, 0)) + } + + confirmed, err := askQuestion(question, defaultChoice) + if err != nil { + return err + } + if !confirmed { + return ErrCanceled + } + return nil +} + +// getUsername returns the username for the provided UID. If the UID does not +// correspond to a user or the username is blank, "UID=<uid>" is returned. +func getUsername(uid int64) string { + u, err := user.LookupId(strconv.Itoa(int(uid))) + if err != nil || u.Username == "" { + return fmt.Sprintf("UID=%d", uid) + } + return u.Username +} + +// formatInfo gives a string description of metadata.ProtectorData. +func formatInfo(data actions.ProtectorInfo) string { + switch data.Source() { + case metadata.SourceType_pam_passphrase: + return "login protector for " + getUsername(data.UID()) + case metadata.SourceType_custom_passphrase: + return fmt.Sprintf("custom protector %q", data.Name()) + case metadata.SourceType_raw_key: + return fmt.Sprintf("raw key protector %q", data.Name()) + default: + panic(ErrInvalidSource) + } +} + +// promptForName gets a name from user input (or flags) and returns it. +func promptForName(ctx *actions.Context) (string, error) { + // A name flag means we do not need to prompt + if nameFlag.Value != "" { + return nameFlag.Value, nil + } + + // Don't ask for a name if we do not need it + if quietFlag.Value || ctx.Config.Source == metadata.SourceType_pam_passphrase { + return "", nil + } + + for { + name, err := promptUser("Enter a name for the new protector: ") + if err != nil { + return "", err + } + if name != "" { + return name, nil + } + } +} + +// promptForSource gets a source type from user input (or flags) and modifies +// the context to use that source. +func promptForSource(ctx *actions.Context) error { + // A source flag overrides everything else. + if sourceFlag.Value != "" { + val, ok := metadata.SourceType_value[sourceFlag.Value] + if !ok || val == 0 { + return ErrInvalidSource + } + ctx.Config.Source = metadata.SourceType(val) + return nil + } + + // Just use the default in quiet mode + if quietFlag.Value { + return nil + } + + // We print all the sources with their number, description, and name. + fmt.Println("Your data can be protected with one of the following sources:") + for idx := 1; idx < len(metadata.SourceType_value); idx++ { + source := metadata.SourceType(idx) + description := sourceDescriptions[source] + fmt.Printf("%d - %s (%s)\n", idx, description, source) + } + + prompt := fmt.Sprintf("Enter the source number for the new protector [%d - %s]: ", + ctx.Config.Source, ctx.Config.Source) + for { + input, err := promptUser(prompt) + if err != nil { + return err + } + + // Use the default if the user just hits enter + if input == "" { + return nil + } + + // Check for a valid index, reprompt if invalid. + index, err := strconv.Atoi(input) + if err == nil && index >= 1 && index < len(metadata.SourceType_value) { + ctx.Config.Source = metadata.SourceType(index) + return nil + } + } +} + +// promptForKeyFile returns an open file that should be used to create or unlock +// a raw_key protector. Be sure to close the file when done. +func promptForKeyFile(prompt string) (*os.File, error) { + // If specified on the command line, we only try no open it once. + if keyFileFlag.Value != "" { + return os.Open(keyFileFlag.Value) + } + if quietFlag.Value { + return nil, ErrSpecifyKeyFile + } + + // Prompt for a valid path until we get a file we can open. + for { + filename, err := promptUser(prompt) + if err != nil { + return nil, err + } + file, err := os.Open(filename) + if err == nil { + return file, nil + } + fmt.Println(err) + } + +} + +// promptForProtector, given a non-empty list of protector options, uses user +// input to select the desired protector. If there is only one option to choose +// from, that protector is automatically selected. +func promptForProtector(options []*actions.ProtectorOption) (int, error) { + numOptions := len(options) + log.Printf("selecting from %s", pluralize(numOptions, "protector")) + + // Get the number of load errors. + numLoadErrors := 0 + for _, option := range options { + if option.LoadError != nil { + log.Printf("when loading option: %v", option.LoadError) + numLoadErrors++ + } + } + + if numLoadErrors == numOptions { + return 0, ErrAllLoadsFailed + } + if numOptions == 1 { + return 0, nil + } + if quietFlag.Value { + return 0, ErrSpecifyProtector + } + + // List all of the protector options which did not have a load error. + fmt.Println("The available protectors are: ") + for idx, option := range options { + if option.LoadError != nil { + continue + } + + description := fmt.Sprintf("%d - %s", idx, formatInfo(option.ProtectorInfo)) + if option.LinkedMount != nil { + description += fmt.Sprintf(" (linked protector on %q)", option.LinkedMount.Path) + } + fmt.Println(description) + } + + if numLoadErrors > 0 { + fmt.Printf(wrapText("NOTE: %d of the %d protectors failed to load. "+loadHelpText, 0)) + } + + for { + input, err := promptUser("Enter the number of protector to use: ") + if err != nil { + return 0, err + } + + // Check for a valid index, reprompt if invalid. + index, err := strconv.Atoi(input) + if err == nil && index >= 0 && index < len(options) { + return index, nil + } + } +} + +// optionFn is an actions.OptionFunc which handles selecting an option for a +// specific policy. This is either done interactively, or by deferring to the +// protectorFlag. +func optionFn(policyDescriptor string, options []*actions.ProtectorOption) (int, error) { + // If we have an unlock-with flag, we directly select the specified + // protector to unlock the policy. + if unlockWithFlag.Value != "" { + log.Printf("optionFn(%s) w/ unlock flag", policyDescriptor) + protector, err := getProtectorFromFlag(unlockWithFlag.Value) + if err != nil { + return 0, err + } + + for idx, option := range options { + if option.Descriptor() == protector.Descriptor() { + return idx, nil + } + } + return 0, actions.ErrNotProtected + } + + log.Printf("optionFn(%s)", policyDescriptor) + return promptForProtector(options) +} diff --git a/cmd/fscrypt/protector.go b/cmd/fscrypt/protector.go new file mode 100644 index 0000000..e77a2e3 --- /dev/null +++ b/cmd/fscrypt/protector.go @@ -0,0 +1,123 @@ +/* + * protector.go - Functions for creating and getting action.Protectors which + * ensure that login passphrases are on the correct 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 + +import ( + "log" + + "fscrypt/actions" + "fscrypt/filesystem" + "fscrypt/metadata" +) + +// createProtector makes a new protector on either ctx.Mount or if the requested +// source is a pam_passphrase, creates it on the root filesystem. Prompts for +// user input are used to get the source, name and keys. +func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error) { + if err := promptForSource(ctx); err != nil { + return nil, err + } + log.Printf("using source: %s", ctx.Config.Source.String()) + + name, err := promptForName(ctx) + if err != nil { + return nil, err + } + log.Printf("using name: %s", name) + + // We only want to create new login protectors on the root filesystem. + // So we make a new context if necessary. + if ctx.Config.Source == metadata.SourceType_pam_passphrase && ctx.Mount.Path != "/" { + log.Printf("creating login protector on %q instead of %q", "/", ctx.Mount.Path) + if ctx, err = modifiedContext(ctx); err != nil { + return nil, err + } + } + + return actions.CreateProtector(ctx, name, createKeyFn) +} + +// selectExistingProtector returns a locked Protector which corresponds to an +// options in the non-empty slice of options. Prompts for user input are used to +// get the keys and select the option. +func selectExistingProtector(ctx *actions.Context, options []*actions.ProtectorOption) (*actions.Protector, error) { + idx, err := promptForProtector(options) + if err != nil { + return nil, err + } + option := options[idx] + + log.Printf("using %s", formatInfo(option.ProtectorInfo)) + return actions.GetProtectorFromOption(ctx, option) +} + +// expandedProtectorOptions gets all the actions.ProtectorOptions for ctx.Mount +// as well as any pam_passphrase protectors for the root filesystem. +func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption, error) { + options, err := ctx.ProtectorOptions() + if err != nil { + return nil, err + } + + // Do nothing different if we are at the root, or cannot load the root. + if ctx.Mount.Path == "/" { + return options, nil + } + if ctx, err = modifiedContext(ctx); err != nil { + log.Print(err) + return options, nil + } + rootOptions, err := ctx.ProtectorOptions() + if err != nil { + log.Print(err) + return options, nil + } + log.Print("adding additional ProtectorOptions") + + // Keep track of what we have seen, so we don't have duplicates + seenOptions := make(map[string]bool) + for _, option := range options { + seenOptions[option.Descriptor()] = true + } + + for _, option := range rootOptions { + // Add in unseen passphrase protectors on the root filesystem + // to the options list as potential linked protectors. + if option.Source() == metadata.SourceType_pam_passphrase && + !seenOptions[option.Descriptor()] { + option.LinkedMount = ctx.Mount + options = append(options, option) + } + } + + return options, nil +} + +// modifiedContext returns a copy of ctx with the mountpoint replaced by that of +// the root filesystem. +func modifiedContext(ctx *actions.Context) (*actions.Context, error) { + mnt, err := filesystem.GetMount("/") + if err != nil { + return nil, err + } + + return &actions.Context{Config: ctx.Config, Mount: mnt}, nil +} diff --git a/cmd/fscrypt/setup.go b/cmd/fscrypt/setup.go new file mode 100644 index 0000000..4f2375c --- /dev/null +++ b/cmd/fscrypt/setup.go @@ -0,0 +1,77 @@ +/* + * strings.go - File containing the functionality initializing directories and + * the global config file. + * + * 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" + "io" + "os" + + "fscrypt/actions" +) + +// createGlobalConfig creates (or overwrites) the global config file +func createGlobalConfig(w io.Writer, path string) error { + if os.Getuid() != 0 { + return ErrMustBeRoot + } + + // Ask to create or replace the config file + _, err := os.Stat(path) + switch { + case err == nil: + err = askConfirmation(fmt.Sprintf("Replace %q?", path), false, "") + if err == nil { + err = os.Remove(path) + } + case os.IsNotExist(err): + err = askConfirmation(fmt.Sprintf("Create %q?", path), true, "") + } + if err != nil { + return err + } + + fmt.Fprintln(w, "Customizing passphrase hashing difficulty for this system...") + err = actions.CreateConfigFile(timeTargetFlag.Value, legacyFlag.Value) + if err != nil { + return err + } + + fmt.Fprintf(w, "Created global config file at %q.\n", path) + return nil +} + +// setupFilesystem creates the directories for a filesystem to use fscrypt. +func setupFilesystem(w io.Writer, path string) error { + ctx, err := actions.NewContextFromMountpoint(path) + if err != nil { + return err + } + + if err = ctx.Mount.Setup(); err != nil { + return err + } + + fmt.Fprintf(w, "Metadata directories created at %q.\n", ctx.Mount.BaseDir()) + fmt.Fprintf(w, "Filesystem %q (%s) ready for use with %s encryption.\n", + ctx.Mount.Path, ctx.Mount.Device, ctx.Mount.Filesystem) + return nil +} diff --git a/cmd/fscrypt/strings.go b/cmd/fscrypt/strings.go new file mode 100644 index 0000000..b77389f --- /dev/null +++ b/cmd/fscrypt/strings.go @@ -0,0 +1,143 @@ +/* + * 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" + "strings" +) + +// Global application strings +const ( + shortUsage = "A tool for managing Linux filesystem encryption" + + apache2GoogleCopyright = `Copyright 2017 Google, Inc. + + 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.` +) + +// Argument usage strings +const ( + directoryArg = "DIRECTORY" + mountpointArg = "MOUNTPOINT" + pathArg = "PATH" + archiveArg = "ARCHIVE_FILE" + recoveryCodeArg = "RECOVERY_CODE" + mountpointIDArg = mountpointArg + ":ID" +) + +// Text Templates which format our command line output (using text/template) +var ( + // indent is the prefix for the output lines in each section + indent = strings.Repeat(" ", indentLength) + // Top level help output: what is printed for "fscrypt" or "fscrypt --help" + appHelpTemplate = `{{.HelpName}} - {{.Usage}} + +Usage: +` + indent + `{{.HelpName}} COMMAND [arguments] [options] + +Commands:{{range .VisibleCommands}} +` + indent + `{{join .Names ", "}}{{"\t- "}}{{.Usage}}{{end}} +{{if .Description}} +Description: +` + indent + `{{.Description}} +{{end}} +Options: +{{range .VisibleFlags}}{{.}} + +{{end}}` + + // Command help output, used when a command has no subcommands + commandHelpTemplate = `{{.HelpName}} - {{.Usage}} + +Usage: +` + indent + `{{.HelpName}}{{if .ArgsUsage}} {{.ArgsUsage}}{{end}}{{if .VisibleFlags}} [options]{{end}} +{{if .Description}} +Description: +` + indent + `{{.Description}} +{{end}}{{if .VisibleFlags}} +Options: +{{range .VisibleFlags}}{{.}} + +{{end}}{{end}}` + + // Subcommand help output, used when a command has subcommands + subcommandHelpTemplate = `{{.HelpName}} - {{.Usage}} + +Usage: +` + indent + `{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}SUBCOMMAND [arguments]{{end}}{{if .VisibleFlags}} [options]{{end}} + +Subcommands:{{range .VisibleCommands}} +` + indent + `{{join .Names ", "}}{{"\t- "}}{{.Usage}}{{end}} +{{if .Description}} +Description: +` + indent + `{{.Description}} +{{end}}{{if .VisibleFlags}} +Options: +{{range .VisibleFlags}}{{.}} + +{{end}}{{end}}` + + // Additional info, used with "fscrypt version" + versionInfoTemplate = `{{.HelpName}} - {{.Usage}} + +{{if .Version}}Version: +` + indent + `{{.Version}} + +{{end}}{{if .Compiled}}Compiled: +` + indent + `{{.Compiled}} + +{{end}}{{if len .Authors}}Author{{with $length := len .Authors}}{{if ne 1 $length}}s{{end}}{{end}}:{{range .Authors}} +` + indent + `{{.}}{{end}} + +{{end}}{{if .Copyright}}Copyright: +` + indent + `{{.Copyright}} + +{{end}}` +) + +// Add words to this map to have pluralize support them. +var plurals = map[string]string{ + "argument": "arguments", + "filesystem": "filesystems", + "protector": "protectors", + "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 { + word = plurals[word] + } + return fmt.Sprintf("%d %s", count, word) +} |