aboutsummaryrefslogtreecommitdiff
path: root/cmd/fscrypt
diff options
context:
space:
mode:
authorJoe Richey joerichey@google.com <joerichey@google.com>2017-06-21 10:21:21 -0700
committerJoe Richey joerichey@google.com <joerichey@google.com>2017-06-28 15:15:21 -0700
commit37c866e1e16a6d2dded11ba93c2e04af3764a139 (patch)
tree745d548ed30e9e70b4702622510690af62a48b58 /cmd/fscrypt
parent93415b198a3ef427c02893b8fdf036aa75ffe50f (diff)
cmd/fscrypt: setup, encrypt, unlock commands
This commit adds in the framework for adding commands and subcommands to the fscrypt tool. This commit adds in the "setup", "encrypt", and "unlock" commands. Additional information can be found by running: fscrypt <command> --help. This commit defines how flags are parsed and errors are handled. It also creates an extensible framework for prompting the user for information. Change-Id: I159d7f44ee2b2bbc5e072f0802850e082d9a13ce
Diffstat (limited to 'cmd/fscrypt')
-rw-r--r--cmd/fscrypt/commands.go296
-rw-r--r--cmd/fscrypt/errors.go188
-rw-r--r--cmd/fscrypt/flags.go261
-rw-r--r--cmd/fscrypt/format.go162
-rw-r--r--cmd/fscrypt/fscrypt.go117
-rw-r--r--cmd/fscrypt/fscrypt_test.go4
-rw-r--r--cmd/fscrypt/keys.go198
-rw-r--r--cmd/fscrypt/prompt.go322
-rw-r--r--cmd/fscrypt/protector.go123
-rw-r--r--cmd/fscrypt/setup.go77
-rw-r--r--cmd/fscrypt/strings.go143
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)
+}