aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-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)
+}