diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/fscrypt/commands.go | 482 | ||||
| -rw-r--r-- | cmd/fscrypt/errors.go | 267 | ||||
| -rw-r--r-- | cmd/fscrypt/flags.go | 92 | ||||
| -rw-r--r-- | cmd/fscrypt/format.go | 98 | ||||
| -rw-r--r-- | cmd/fscrypt/fscrypt.go | 51 | ||||
| -rw-r--r-- | cmd/fscrypt/fscrypt_bash_completion | 332 | ||||
| -rw-r--r-- | cmd/fscrypt/keys.go | 67 | ||||
| -rw-r--r-- | cmd/fscrypt/prompt.go | 10 | ||||
| -rw-r--r-- | cmd/fscrypt/protector.go | 38 | ||||
| -rw-r--r-- | cmd/fscrypt/setup.go | 65 | ||||
| -rw-r--r-- | cmd/fscrypt/status.go | 72 | ||||
| -rw-r--r-- | cmd/fscrypt/strings.go | 38 |
12 files changed, 1241 insertions, 371 deletions
diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index 2f23a0f..3fc68a9 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -24,55 +24,72 @@ import ( "fmt" "log" "os" + "path/filepath" + "strings" "github.com/pkg/errors" "github.com/urfave/cli" "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/crypto" "github.com/google/fscrypt/filesystem" + "github.com/google/fscrypt/keyring" "github.com/google/fscrypt/metadata" "github.com/google/fscrypt/security" "github.com/google/fscrypt/util" ) -// Setup is a command which can to global or per-filesystem initialization. +// Setup is a command which can do 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, + file and/or prepares a filesystem for use with fscrypt. + + (1) When used without %[1]s, this command creates the global + config file %[2]s and the fscrypt metadata directory for the + root filesystem (i.e. /.fscrypt). This requires root privileges. + The passphrase hashing parameters in %[2]s are automatically set + to an appropriate hardness, as determined by %[3]s. The root + filesystem's metadata directory is created even if the root + filesystem doesn't support encryption itself, since it's where + login passphrase protectors are stored. + + (2) When used with %[1]s, this command creates the fscrypt + metadata directory for the filesystem mounted at %[1]s. This + allows fscrypt to be used on that filesystem, provided that any + kernel and filesystem-specific prerequisites are also met (see + the README). This may require root privileges.`, + mountpointArg, actions.ConfigFileLocation, shortDisplay(timeTargetFlag)), - Flags: []cli.Flag{timeTargetFlag, legacyFlag, forceFlag}, + Flags: []cli.Flag{timeTargetFlag, forceFlag, allUsersSetupFlag}, 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) + if err := createGlobalConfig(c.App.Writer, actions.ConfigFileLocation); err != nil { + return newExitError(c, err) + } + if err := setupFilesystem(c.App.Writer, actions.LoginProtectorMountpoint); err != nil { + if _, ok := err.(*filesystem.ErrAlreadySetup); !ok { + return newExitError(c, err) + } + fmt.Fprintf(c.App.Writer, + "Skipping creating %s because it already exists.\n", + filepath.Join(actions.LoginProtectorMountpoint, ".fscrypt")) + } case 1: // Case (2) - filesystem setup - err = setupFilesystem(c.App.Writer, c.Args().Get(0)) + if err := setupFilesystem(c.App.Writer, c.Args().Get(0)); err != nil { + return newExitError(c, err) + } default: return expectedArgsErr(c, 1, true) } - - if err != nil { - return newExitError(c, err) - } return nil } @@ -90,7 +107,7 @@ var Encrypt = cli.Command{ immediately be used.`, directoryArg, shortDisplay(policyFlag), shortDisplay(protectorFlag), mountpointArg), Flags: []cli.Flag{policyFlag, unlockWithFlag, protectorFlag, sourceFlag, - userFlag, nameFlag, keyFileFlag, skipUnlockFlag}, + userFlag, nameFlag, keyFileFlag, skipUnlockFlag, noRecoveryFlag}, Action: encryptAction, } @@ -104,6 +121,13 @@ func encryptAction(c *cli.Context) error { return newExitError(c, err) } + // Most people expect that other users can't see their encrypted files + // while they're unlocked, so change the directory's mode to 0700. + if err := os.Chmod(path, 0700); err != nil { + fmt.Fprintf(c.App.Writer, "Warning: unable to chmod %q to 0700 [%v]\n", path, err) + // Continue on; don't consider this a fatal error. + } + if !skipUnlockFlag.Value { fmt.Fprintf(c.App.Writer, "%q is now encrypted, unlocked, and ready for use.\n", path) @@ -115,15 +139,71 @@ func encryptAction(c *cli.Context) error { return nil } +// validateKeyringPrereqs ensures we're ready to add, remove, or get the status +// of the key for the given encryption policy (if policy != nil) or for the +// current default encryption policy (if policy == nil). +func validateKeyringPrereqs(ctx *actions.Context, policy *actions.Policy) error { + var policyVersion int64 + if policy == nil { + policyVersion = ctx.Config.Options.PolicyVersion + } else { + policyVersion = policy.Version() + } + // If it's a v2 policy, we're good to go, since non-root users can + // add/remove v2 policy keys directly to/from the filesystem, where they + // are usable by the filesystem on behalf of any process. + if policyVersion != 1 { + return nil + } + if ctx.Config.GetUseFsKeyringForV1Policies() { + // We'll be using the filesystem keyring, but it's a v1 + // encryption policy so root is required. + if !util.IsUserRoot() { + return ErrFsKeyringPerm + } + return nil + } + // We'll be using the target user's user keyring, so make sure a user + // was explicitly specified if the command is being run as root, and + // make sure that user's keyring is accessible. + if userFlag.Value == "" && util.IsUserRoot() { + return ErrSpecifyUser + } + if _, err := keyring.UserKeyringID(ctx.TargetUser, true); err != nil { + return err + } + return nil +} + +func writeRecoveryInstructions(recoveryPassphrase *crypto.Key, recoveryProtector *actions.Protector, + policy *actions.Policy, dirPath string) error { + if recoveryPassphrase == nil { + return nil + } + recoveryFile := filepath.Join(dirPath, "fscrypt_recovery_readme.txt") + if err := actions.WriteRecoveryInstructions(recoveryPassphrase, recoveryProtector, + policy, recoveryFile); err != nil { + return err + } + msg := fmt.Sprintf(`See %q for important recovery instructions. + It is *strongly recommended* to record the recovery passphrase in a + secure location; otherwise you will lose access to this directory if you + reinstall the operating system or move this filesystem to another + system.`, recoveryFile) + hdr := "IMPORTANT: " + fmt.Print("\n" + hdr + wrapText(msg, len(hdr)) + "\n\n") + 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) { - target, err := parseUserFlag(!skipUnlockFlag.Value) + targetUser, err := parseUserFlag() if err != nil { return } - ctx, err := actions.NewContextFromPath(path, target) + ctx, err := actions.NewContextFromPath(path, targetUser) if err != nil { return } @@ -132,20 +212,37 @@ func encryptPath(path string) (err error) { } var policy *actions.Policy + var recoveryPassphrase *crypto.Key + var recoveryProtector *actions.Protector if policyFlag.Value != "" { log.Printf("getting policy for %q", path) - policy, err = getPolicyFromFlag(policyFlag.Value, ctx.TargetUser) + if policy, err = getPolicyFromFlag(policyFlag.Value, ctx.TargetUser); err != nil { + return + } + defer policy.Lock() + + if !skipUnlockFlag.Value { + if err = validateKeyringPrereqs(ctx, policy); err != nil { + return + } + } } else { log.Printf("creating policy for %q", path) + if !skipUnlockFlag.Value { + if err = validateKeyringPrereqs(ctx, nil); err != nil { + return + } + } + protector, created, protErr := selectOrCreateProtector(ctx) - // Successfully created protector should be reverted on failure. if protErr != nil { return protErr } defer func() { protector.Lock() + // Successfully created protector should be reverted on failure. if err != nil && created { protector.Revert() } @@ -154,39 +251,70 @@ func encryptPath(path string) (err error) { 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() - if err != nil { - policy.Deprovision() - policy.Revert() + if policy, err = actions.CreatePolicy(ctx, protector); err != nil { + return } - }() + defer func() { + policy.Lock() + // Successfully created policy should be reverted on failure. + if err != nil { + policy.Revert() + } + }() - // Unlock() first, so if the Unlock() fails the directory isn't changed. - if !skipUnlockFlag.Value { + // Generate a recovery passphrase if needed. + if ctx.Mount != protector.Context.Mount && !noRecoveryFlag.Value { + if recoveryPassphrase, recoveryProtector, err = actions.AddRecoveryPassphrase( + policy, filepath.Base(path)); err != nil { + return + } + defer func() { + recoveryPassphrase.Wipe() + recoveryProtector.Lock() + // Successfully created protector should be reverted on failure. + if err != nil { + recoveryProtector.Revert() + } + }() + } + } + + // Unlock() and Provision() first, so if that if these fail the + // directory isn't changed, and also because v2 policies can't be + // applied while deprovisioned unless the process is running as root. + if !skipUnlockFlag.Value || !policy.CanBeAppliedWithoutProvisioning() { if err = policy.Unlock(optionFn, existingKeyFn); err != nil { return } if err = policy.Provision(); err != nil { return } + defer func() { + if err != nil || skipUnlockFlag.Value { + policy.Deprovision(false) + } + }() } - if err = policy.Apply(path); os.IsPermission(errors.Cause(err)) { - // EACCES at this point indicates ownership issues. - err = errors.Wrap(ErrBadOwners, path) + if err = policy.Apply(path); err != nil { + return } - return + return writeRecoveryInstructions(recoveryPassphrase, recoveryProtector, policy, path) } // 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) + + log.Printf("checking whether %q is already encrypted", path) + if _, err := metadata.GetPolicy(path); err == nil { + return &metadata.ErrAlreadyEncrypted{Path: path} + } + + log.Printf("checking whether filesystem %s supports encryption", ctx.Mount.Path) + if err := ctx.Mount.CheckSupport(); err != nil { + return err + } + + log.Printf("checking whether %q is an empty and readable directory", path) f, err := os.Open(path) if err != nil { return err @@ -196,29 +324,17 @@ func checkEncryptable(ctx *actions.Context, path string) error { 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 supports encryption and filesystem is using fscrypt", path) - switch _, err := actions.GetPolicyFromPath(ctx, path); errors.Cause(err) { - case metadata.ErrNotEncrypted: - // We are not encrypted. Finally, we check that the filesystem - // supports encryption - return ctx.Mount.CheckSupport() - case nil: - // We are encrypted - return errors.Wrap(metadata.ErrEncrypted, path) - default: + err = errors.Wrap(err, path) + log.Print(err) return err + case len(names) > 0: + return &ErrDirNotEmpty{path} } + return err } // selectOrCreateProtector uses user input (or flags) to either create a new -// protector or select and existing one. The boolean return value is true if we +// protector or select an existing one. The boolean return value is true if we // created a new protector. func selectOrCreateProtector(ctx *actions.Context) (*actions.Protector, bool, error) { if protectorFlag.Value != "" { @@ -262,8 +378,8 @@ var Unlock = cli.Command{ 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, + locked again upon reboot, or after running "fscrypt lock" or + "fscrypt purge".`, directoryArg, shortDisplay(unlockWithFlag)), Flags: []cli.Flag{unlockWithFlag, keyFileFlag, userFlag}, Action: unlockAction, @@ -274,12 +390,12 @@ func unlockAction(c *cli.Context) error { return expectedArgsErr(c, 1, false) } - target, err := parseUserFlag(true) + targetUser, err := parseUserFlag() if err != nil { return newExitError(c, err) } path := c.Args().Get(0) - ctx, err := actions.NewContextFromPath(path, target) + ctx, err := actions.NewContextFromPath(path, targetUser) if err != nil { return newExitError(c, err) } @@ -290,10 +406,15 @@ func unlockAction(c *cli.Context) error { if err != nil { return newExitError(c, err) } + // Ensure the keyring is ready. + if err = validateKeyringPrereqs(ctx, policy); err != nil { + return newExitError(c, err) + } // Check if directory is already unlocked - if policy.IsProvisioned() { - log.Printf("policy %s is already provisioned", policy.Descriptor()) - return newExitError(c, errors.Wrapf(ErrPolicyUnlocked, path)) + if policy.IsProvisionedByTargetUser() { + log.Printf("policy %s is already provisioned by %v", + policy.Descriptor(), ctx.TargetUser.Username) + return newExitError(c, errors.Wrap(ErrDirAlreadyUnlocked, path)) } if err := policy.Unlock(optionFn, existingKeyFn); err != nil { @@ -309,6 +430,163 @@ func unlockAction(c *cli.Context) error { return nil } +func dropCachesIfRequested(c *cli.Context, ctx *actions.Context) error { + if dropCachesFlag.Value { + if err := security.DropFilesystemCache(); err != nil { + return err + } + fmt.Fprintf(c.App.Writer, "Encrypted data removed from filesystem cache.\n") + } else { + fmt.Fprintf(c.App.Writer, "Filesystem %q should now be unmounted.\n", ctx.Mount.Path) + } + return nil +} + +// Lock takes an encrypted directory and locks it, undoing Unlock. +var Lock = cli.Command{ + Name: "lock", + ArgsUsage: directoryArg, + Usage: "lock an encrypted directory", + Description: fmt.Sprintf(`This command takes %s, an encrypted directory + which has been unlocked by fscrypt, and locks the directory by + removing the encryption key from the kernel. I.e., it undoes the + effect of 'fscrypt unlock'. + + For this to be effective, all files in the directory must first + be closed. + + If the directory uses a v1 encryption policy, then the %s=true + option may be needed to properly lock it. Root is required for + this. + + If the directory uses a v2 encryption policy, then a non-root + user can lock it, but only if it's the same user who unlocked it + originally and if no other users have unlocked it too. + + WARNING: even after the key has been removed, decrypted data may + still be present in freed memory, where it may still be + recoverable by an attacker who compromises system memory. To be + fully safe, you must reboot with a power cycle.`, + directoryArg, shortDisplay(dropCachesFlag)), + Flags: []cli.Flag{dropCachesFlag, userFlag, allUsersLockFlag}, + Action: lockAction, +} + +func lockAction(c *cli.Context) error { + if c.NArg() != 1 { + return expectedArgsErr(c, 1, false) + } + + targetUser, err := parseUserFlag() + if err != nil { + return newExitError(c, err) + } + path := c.Args().Get(0) + ctx, err := actions.NewContextFromPath(path, targetUser) + 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) + } + // Ensure the keyring is ready. + if err = validateKeyringPrereqs(ctx, policy); err != nil { + return newExitError(c, err) + } + // Check for permission to drop caches, if it may be needed. + if policy.NeedsUserKeyring() && dropCachesFlag.Value && !util.IsUserRoot() { + return newExitError(c, ErrDropCachesPerm) + } + + if err = policy.Deprovision(allUsersLockFlag.Value); err != nil { + switch err { + case keyring.ErrKeyNotPresent: + break + case keyring.ErrKeyAddedByOtherUsers: + return newExitError(c, &ErrDirUnlockedByOtherUsers{path}) + case keyring.ErrKeyFilesOpen: + return newExitError(c, &ErrDirFilesOpen{path}) + default: + return newExitError(c, err) + } + // Key is no longer present. Normally that means the directory + // is already locked; in that case we exit with an error. But + // if the policy uses the user keyring (v1 policies only), then + // the directory might have been incompletely locked earlier, + // due to open files. Try to detect that case and finish + // locking the directory by dropping caches again. + if !policy.NeedsUserKeyring() || !isDirUnlockedHeuristic(path) { + log.Printf("policy %s is already fully deprovisioned", policy.Descriptor()) + return newExitError(c, errors.Wrap(ErrDirAlreadyLocked, path)) + } + } + + if policy.NeedsUserKeyring() { + if err = dropCachesIfRequested(c, ctx); err != nil { + return newExitError(c, err) + } + if isDirUnlockedHeuristic(path) { + return newExitError(c, &ErrDirFilesOpen{path}) + } + } + + fmt.Fprintf(c.App.Writer, "%q is now locked.\n", path) + return nil +} + +func isPossibleNoKeyName(filename string) bool { + // No-key names are at least 22 bytes long, since they are + // base64-encoded and ciphertext filenames are at least 16 bytes. + if len(filename) < 22 { + return false + } + // On the latest kernels, no-key names contain only base64url characters + // (A-Z, a-z, 0-9, -, and _). On older kernels, the + and , characters + // were used too. Allow all of these characters. + validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_+," + for _, char := range filename { + if !strings.ContainsRune(validChars, char) { + return false + } + } + return true +} + +// isDirUnlockedHeuristic returns true if the directory is definitely still +// unlocked. This is the case if we can create a subdirectory or if the +// directory contains filenames that aren't valid no-key names. It returns +// false if the directory is probably locked (though it could also be unlocked). +// +// This is only useful if the directory's policy uses the user keyring, since +// otherwise the status can be easily found via the filesystem keyring. +func isDirUnlockedHeuristic(dirPath string) bool { + subdirPath := filepath.Join(dirPath, "fscrypt-is-dir-unlocked") + if err := os.Mkdir(subdirPath, 0700); err == nil { + os.Remove(subdirPath) + return true + } + dir, err := os.Open(dirPath) + if err != nil { + return false + } + defer dir.Close() + + names, err := dir.Readdirnames(-1) + if err != nil { + return false + } + for _, name := range names { + if !isPossibleNoKeyName(name) { + return true + } + } + return false +} + // Purge removes all the policy keys from the keyring (also need unmount). var Purge = cli.Command{ Name: "purge", @@ -358,15 +636,18 @@ func purgeAction(c *cli.Context) error { } } - target, err := parseUserFlag(true) + targetUser, err := parseUserFlag() if err != nil { return newExitError(c, err) } mountpoint := c.Args().Get(0) - ctx, err := actions.NewContextFromMountpoint(mountpoint, target) + ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser) if err != nil { return newExitError(c, err) } + if err = validateKeyringPrereqs(ctx, nil); err != nil { + return newExitError(c, err) + } question := fmt.Sprintf("Purge all policy keys from %q", ctx.Mount.Path) if dropCachesFlag.Value { @@ -382,13 +663,8 @@ func purgeAction(c *cli.Context) error { } fmt.Fprintf(c.App.Writer, "Policies purged for %q.\n", ctx.Mount.Path) - if dropCachesFlag.Value { - if err = security.DropFilesystemCache(); err != nil { - return newExitError(c, err) - } - fmt.Fprintf(c.App.Writer, "Encrypted data removed filesystem cache.\n") - } else { - fmt.Fprintf(c.App.Writer, "Filesystem %q should now be unmounted.\n", ctx.Mount.Path) + if err = dropCachesIfRequested(c, ctx); err != nil { + return newExitError(c, err) } return nil } @@ -429,17 +705,15 @@ func statusAction(c *cli.Context) error { err = writeGlobalStatus(c.App.Writer) case 1: path := c.Args().Get(0) - ctx, mntErr := actions.NewContextFromMountpoint(path, nil) - switch errors.Cause(mntErr) { - case nil: + var ctx *actions.Context + ctx, err = actions.NewContextFromMountpoint(path, nil) + if err == nil { // Case (2) - mountpoint status err = writeFilesystemStatus(c.App.Writer, ctx) - case filesystem.ErrNotAMountpoint: + } else if _, ok := err.(*filesystem.ErrNotAMountpoint); ok { // Case (3) - file or directory status err = writePathStatus(c.App.Writer, path) - default: - err = mntErr } default: return expectedArgsErr(c, 1, true) @@ -474,7 +748,7 @@ var Metadata = cli.Command{ (4) Changing the protector protecting a policy using the "add-protector-to-policy" and "remove-protector-from-policy" subcommands.`, - Subcommands: []cli.Command{createMetadata, destoryMetadata, changePassphrase, + Subcommands: []cli.Command{createMetadata, destroyMetadata, changePassphrase, addProtectorToPolicy, removeProtectorFromPolicy, dumpMetadata}, } @@ -508,18 +782,18 @@ func createProtectorAction(c *cli.Context) error { return expectedArgsErr(c, 1, false) } - target, err := parseUserFlag(false) + targetUser, err := parseUserFlag() if err != nil { return newExitError(c, err) } mountpoint := c.Args().Get(0) - ctx, err := actions.NewContextFromMountpoint(mountpoint, target) + ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser) if err != nil { return newExitError(c, err) } prompt := fmt.Sprintf("Create new protector on %q", ctx.Mount.Path) - if err := askConfirmation(prompt, true, ""); err != nil { + if err = askConfirmation(prompt, true, ""); err != nil { return newExitError(c, err) } @@ -537,8 +811,8 @@ func createProtectorAction(c *cli.Context) error { var createPolicy = cli.Command{ Name: "policy", ArgsUsage: fmt.Sprintf("%s %s", mountpointArg, shortDisplay(protectorFlag)), - Usage: "create a new protector on a filesystem", - Description: fmt.Sprintf(`This command creates a new protector on %s + Usage: "create a new policy on a filesystem", + Description: fmt.Sprintf(`This command creates a new policy on %s that has not (yet) been applied to any directory. After creation, the user can use %s with "fscrypt encrypt" to encrypt a directory with this new policy. As all policies must be @@ -561,20 +835,20 @@ func createPolicyAction(c *cli.Context) error { return newExitError(c, err) } - if err := checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil { + if err = checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil { return err } protector, err := getProtectorFromFlag(protectorFlag.Value, ctx.TargetUser) if err != nil { return newExitError(c, err) } - if err := protector.Unlock(existingKeyFn); err != nil { + if err = protector.Unlock(existingKeyFn); err != nil { return newExitError(c, err) } defer protector.Lock() prompt := fmt.Sprintf("Create new policy on %q", ctx.Mount.Path) - if err := askConfirmation(prompt, true, ""); err != nil { + if err = askConfirmation(prompt, true, ""); err != nil { return newExitError(c, err) } @@ -589,7 +863,7 @@ func createPolicyAction(c *cli.Context) error { return nil } -var destoryMetadata = cli.Command{ +var destroyMetadata = cli.Command{ Name: "destroy", ArgsUsage: fmt.Sprintf("[%s | %s | %s]", shortDisplay(protectorFlag), shortDisplay(policyFlag), mountpointArg), @@ -616,10 +890,10 @@ var destoryMetadata = cli.Command{ shortDisplay(protectorFlag), shortDisplay(policyFlag), mountpointArg), Flags: []cli.Flag{protectorFlag, policyFlag, forceFlag}, - Action: destoryMetadataAction, + Action: destroyMetadataAction, } -func destoryMetadataAction(c *cli.Context) error { +func destroyMetadataAction(c *cli.Context) error { switch c.NArg() { case 0: switch { @@ -759,6 +1033,9 @@ func addProtectorAction(c *cli.Context) error { } // Sanity check before unlocking everything if err := policy.AddProtector(protector); errors.Cause(err) != actions.ErrLocked { + if err == nil { + err = errors.New("policy and protector are not locked") + } return newExitError(c, err) } @@ -806,29 +1083,30 @@ func removeProtectorAction(c *cli.Context) error { return err } - // We do not need to unlock anything for this operation - protector, err := getProtectorFromFlag(protectorFlag.Value, nil) + // We only need the protector descriptor, not the protector itself. + ctx, protectorDescriptor, err := parseMetadataFlag(protectorFlag.Value, nil) if err != nil { return newExitError(c, err) } - policy, err := getPolicyFromFlag(policyFlag.Value, protector.Context.TargetUser) + // We don't need to unlock the policy for this operation. + policy, err := getPolicyFromFlag(policyFlag.Value, ctx.TargetUser) if err != nil { return newExitError(c, err) } prompt := fmt.Sprintf("Stop protecting policy %s with protector %s?", - policy.Descriptor(), protector.Descriptor()) + policy.Descriptor(), protectorDescriptor) warning := "All files using this policy will NO LONGER be accessible with this protector!!" if err := askConfirmation(prompt, false, warning); err != nil { return newExitError(c, err) } - if err := policy.RemoveProtector(protector); err != nil { + if err := policy.RemoveProtector(protectorDescriptor); err != nil { return newExitError(c, err) } fmt.Fprintf(c.App.Writer, "Protector %s no longer protecting policy %s.\n", - protector.Descriptor(), policy.Descriptor()) + protectorDescriptor, policy.Descriptor()) return nil } diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go index 81a6798..e4da884 100644 --- a/cmd/fscrypt/errors.go +++ b/cmd/fscrypt/errors.go @@ -30,12 +30,13 @@ import ( "github.com/pkg/errors" "github.com/urfave/cli" + "golang.org/x/sys/unix" "github.com/google/fscrypt/actions" "github.com/google/fscrypt/crypto" "github.com/google/fscrypt/filesystem" + "github.com/google/fscrypt/keyring" "github.com/google/fscrypt/metadata" - "github.com/google/fscrypt/security" "github.com/google/fscrypt/util" ) @@ -45,8 +46,7 @@ const failureExitCode = 1 // Various errors used for the top level user interface var ( ErrCanceled = errors.New("operation canceled") - ErrNoDesctructiveOps = errors.New("operation would be destructive") - ErrMaxPassphrase = util.SystemError("max passphrase length exceeded") + ErrNoDestructiveOps = errors.New("operation would be destructive") ErrInvalidSource = errors.New("invalid source type") ErrPassphraseMismatch = errors.New("entered passphrases do not match") ErrSpecifyProtector = errors.New("multiple protectors available") @@ -55,15 +55,47 @@ var ( ErrKeyFileLength = errors.Errorf("key file must be %d bytes", metadata.InternalKeyLen) ErrAllLoadsFailed = errors.New("could not load any protectors") ErrMustBeRoot = errors.New("this command must be run as root") - ErrPolicyUnlocked = errors.New("this file or directory is already unlocked") - ErrBadOwners = errors.New("you do not own this directory") - ErrNotEmptyDir = errors.New("not an empty directory") + ErrDirAlreadyUnlocked = errors.New("this file or directory is already unlocked") + ErrDirAlreadyLocked = errors.New("this file or directory is already locked") ErrNotPassphrase = errors.New("protector does not use a passphrase") ErrUnknownUser = errors.New("unknown user") ErrDropCachesPerm = errors.New("inode cache can only be dropped as root") ErrSpecifyUser = errors.New("user must be specified when run as root") + ErrFsKeyringPerm = errors.New("root is required to add/remove v1 encryption policy keys to/from filesystem") ) +// ErrDirFilesOpen indicates that a directory can't be fully locked because +// files protected by the directory's policy are still open. +type ErrDirFilesOpen struct { + DirPath string +} + +func (err *ErrDirFilesOpen) Error() string { + return `Directory was incompletely locked because some files are still + open. These files remain accessible.` +} + +// ErrDirUnlockedByOtherUsers indicates that a directory can't be locked because +// the directory's policy is still provisioned by other users. +type ErrDirUnlockedByOtherUsers struct { + DirPath string +} + +func (err *ErrDirUnlockedByOtherUsers) Error() string { + return fmt.Sprintf(`Directory %q couldn't be fully locked because other + user(s) have unlocked it.`, err.DirPath) +} + +// ErrDirNotEmpty indicates that a directory can't be encrypted because it's not +// empty. +type ErrDirNotEmpty struct { + DirPath string +} + +func (err *ErrDirNotEmpty) Error() string { + return fmt.Sprintf("Directory %q cannot be encrypted because it is non-empty.", err.DirPath) +} + 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. @@ -74,67 +106,188 @@ func getFullName(c *cli.Context) string { return c.App.HelpName } +func isGrubInstalledOnFilesystem(mnt *filesystem.Mount) bool { + dir := filepath.Join(mnt.Path, "boot/grub") + grubDirMount, _ := filesystem.FindMount(dir) + return grubDirMount == mnt +} + +func suggestEnablingEncryption(mnt *filesystem.Mount) string { + kconfig := "CONFIG_FS_ENCRYPTION=y" + switch mnt.FilesystemType { + case "ext4": + // Recommend running tune2fs -O encrypt. But be really careful; + // old kernels didn't support block_size != PAGE_SIZE, and old + // GRUB didn't support encryption. + var statfs unix.Statfs_t + if err := unix.Statfs(mnt.Path, &statfs); err != nil { + return "" + } + pagesize := os.Getpagesize() + if int64(statfs.Bsize) != int64(pagesize) && !util.IsKernelVersionAtLeast(5, 5) { + return fmt.Sprintf(`This filesystem uses a block size + (%d) other than the system page size (%d). Ext4 + encryption didn't support this case until kernel v5.5. + Do *not* enable encryption on this filesystem. Either + upgrade your kernel to v5.5 or later, or re-create this + filesystem using 'mkfs.ext4 -b %d -O encrypt %s' + (WARNING: that will erase all data on it).`, + statfs.Bsize, pagesize, pagesize, mnt.Device) + } + if !util.IsKernelVersionAtLeast(5, 1) { + kconfig = "CONFIG_EXT4_ENCRYPTION=y" + } + s := fmt.Sprintf(`To enable encryption support on this + filesystem, run: + + > sudo tune2fs -O encrypt %q + `, mnt.Device) + if isGrubInstalledOnFilesystem(mnt) { + s += ` + WARNING: you seem to have GRUB installed on this + filesystem. Before doing the above, make sure you are + using GRUB v2.04 or later; otherwise your system will + become unbootable. + ` + } + s += fmt.Sprintf(` + Also ensure that your kernel has %s. See the documentation for + more details.`, kconfig) + return s + case "f2fs": + if !util.IsKernelVersionAtLeast(5, 1) { + kconfig = "CONFIG_F2FS_FS_ENCRYPTION=y" + } + return fmt.Sprintf(`To enable encryption support on this + filesystem, you'll need to run: + + > sudo fsck.f2fs -O encrypt %q + + Also ensure that your kernel has %s. See the documentation for + more details.`, mnt.Device, kconfig) + default: + return `See the documentation for how to enable encryption + support on this filesystem.` + } +} + // 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 e := err.(type) { + case *ErrDirFilesOpen: + return fmt.Sprintf(`Try killing any processes using files in the + directory, for example using: + + > find %q -print0 | xargs -0 fuser -k + + Then re-run: + + > fscrypt lock %q`, e.DirPath, e.DirPath) + case *ErrDirNotEmpty: + dir := filepath.Clean(e.DirPath) + newDir := dir + ".new" + return fmt.Sprintf(`Files cannot be encrypted in-place. Instead, + encrypt a new directory, copy the files into it, and securely + delete the original directory. For example: + + > mkdir %q + > fscrypt encrypt %q + > cp -a -T %q %q + > find %q -type f -print0 | xargs -0 shred -n1 --remove=unlink + > rm -rf %q + > mv %q %q + + Caution: due to the nature of modern storage devices and filesystems, + the original data may still be recoverable from disk. It's much better + to encrypt your files from the start.`, newDir, newDir, dir, newDir, dir, dir, newDir, dir) + case *ErrDirUnlockedByOtherUsers: + return fmt.Sprintf(`If you want to force the directory to be + locked, use: + + > sudo fscrypt lock --all-users %q`, e.DirPath) + case *actions.ErrBadConfigFile: + return `Either fix this file manually, or run "sudo fscrypt setup" to recreate it.` + case *actions.ErrLoginProtectorName: + return fmt.Sprintf("To fix this, don't specify the %s option.", shortDisplay(nameFlag)) + case *actions.ErrMissingProtectorName: + return fmt.Sprintf("Use %s to specify a protector name.", shortDisplay(nameFlag)) + case *actions.ErrNoConfigFile: + return `Run "sudo fscrypt setup" to create this file.` + case *filesystem.ErrEncryptionNotEnabled: + return suggestEnablingEncryption(e.Mount) + case *filesystem.ErrEncryptionNotSupported: + switch e.Mount.FilesystemType { + case "ext4": + if !util.IsKernelVersionAtLeast(4, 1) { + return "ext4 encryption requires kernel v4.1 or later." + } + case "f2fs": + if !util.IsKernelVersionAtLeast(4, 2) { + return "f2fs encryption requires kernel v4.2 or later." + } + case "ubifs": + if !util.IsKernelVersionAtLeast(4, 10) { + return "ubifs encryption requires kernel v4.10 or later." + } + case "ceph": + if !util.IsKernelVersionAtLeast(6, 6) { + return "CephFS encryption requires kernel v6.6 or later." + } + } + return "" + case *filesystem.ErrNoCreatePermission: + return `For how to allow users to create fscrypt metadata on a + filesystem, refer to + https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem` + case *filesystem.ErrNotSetup: + return fmt.Sprintf(`Run "sudo fscrypt setup %s" to use fscrypt + on this filesystem.`, e.Mount.Path) + case *keyring.ErrAccessUserKeyring: + return fmt.Sprintf(`You can only use %s to access the user + keyring of another user if you are running as root.`, + shortDisplay(userFlag)) + case *keyring.ErrSessionUserKeyring: + return `This is usually the result of a bad PAM configuration. + Either correct the problem in your PAM stack, enable + pam_keyinit.so, or run "keyctl link @u @s".` + case *metadata.ErrLockedRegularFile: + return `It is not possible to operate directly on a locked + regular file, since the kernel does not support this. + Specify the parent directory instead. (For loose files, + any directory with the file's policy works.)` + } switch errors.Cause(err) { - case filesystem.ErrNotSetup: - return fmt.Sprintf(`Run "fscrypt setup %s" to use fscrypt on this filesystem.`, mountpointArg) - case crypto.ErrKeyLock: + case crypto.ErrMlockUlimit: return `Too much memory was requested to be locked in RAM. The current limit for this user can be checked with "ulimit -l". The limit can be modified by either changing the "memlock" item in /etc/security/limits.conf or by changing the "LimitMEMLOCK" value in systemd.` - 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 security.ErrSessionUserKeying: - return `This is usually the result of a bad PAM configuration. - Either correct the problem in your PAM stack, enable - pam_keyinit.so, or run "keyctl link @u @s".` - case security.ErrAccessUserKeyring: - return fmt.Sprintf(`You can only use %s to access the user - keyring of another user if you are running as root.`, - shortDisplay(userFlag)) - case actions.ErrBadConfigFile: - return `Run "sudo fscrypt setup" to recreate the file.` - 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 keyring.ErrV2PoliciesUnsupported: + return fmt.Sprintf(`v2 encryption policies are only supported by kernel + version 5.4 and later. Either use a newer kernel, or change + policy_version to 1 in %s.`, actions.ConfigFileLocation) + case ErrNoDestructiveOps: + return fmt.Sprintf("If desired, use %s to automatically run destructive operations.", + shortDisplay(forceFlag)) case ErrSpecifyProtector: - return fmt.Sprintf("Use %s to specify a protector.", shortDisplay(protectorFlag)) + return fmt.Sprintf("Use %s or %s to specify a protector.", + shortDisplay(protectorFlag), shortDisplay(unlockWithFlag)) 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 ErrDropCachesPerm: return fmt.Sprintf(`Either this command should be run as root to properly clear the inode cache, or it should be run with %s=false (this may leave encrypted files and directories in an accessible state).`, shortDisplay(dropCachesFlag)) + case ErrFsKeyringPerm: + return `Either this command should be run as root, or you should + set '"use_fs_keyring_for_v1_policies": false' in + /etc/fscrypt.conf, or you should re-create your + encrypted directories using v2 encryption policies + rather than v1 (this requires setting '"policy_version": + "2"' in the "options" section of /etc/fscrypt.conf).` case ErrSpecifyUser: return fmt.Sprintf(`When running this command as root, you usually still want to provision/remove keys for a normal @@ -151,12 +304,12 @@ func getErrorSuggestions(err error) string { } // 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. +// returned error prepends an error tag and the name of the relevant command, +// and it 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)) + // Prepend the error tag and full name, and append suggestions (if any) + prefix := "[ERROR] " + getFullName(c) + ": " + message := prefix + wrapText(err.Error(), utf8.RuneCountInString(prefix)) if suggestion := getErrorSuggestions(err); suggestion != "" { message += "\n\n" + wrapText(suggestion, 0) @@ -165,8 +318,8 @@ func newExitError(c *cli.Context, err error) error { 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. +// usageError implements cli.ExitCoder to print the usage and return a non-zero +// value. This error should be used when a command is used incorrectly. type usageError struct { c *cli.Context message string diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go index 5137eff..3d3c51d 100644 --- a/cmd/fscrypt/flags.go +++ b/cmd/fscrypt/flags.go @@ -33,7 +33,6 @@ import ( "github.com/urfave/cli" "github.com/google/fscrypt/actions" - "github.com/google/fscrypt/security" "github.com/google/fscrypt/util" ) @@ -115,9 +114,10 @@ var ( // 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, + forceFlag, skipUnlockFlag, timeTargetFlag, sourceFlag, nameFlag, keyFileFlag, protectorFlag, - unlockWithFlag, policyFlag} + unlockWithFlag, policyFlag, allUsersLockFlag, allUsersSetupFlag, + noRecoveryFlag} // universalFlags contains flags that should be on every command universalFlags = []cli.Flag{verboseFlag, quietFlag, helpFlag} ) @@ -130,7 +130,7 @@ var ( } versionFlag = &boolFlag{ Name: "version", - Usage: `Prints version and license information.`, + Usage: `Prints version information.`, } verboseFlag = &boolFlag{ Name: "verbose", @@ -144,16 +144,10 @@ var ( } 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, + Usage: `Suppresses all confirmation prompts and warnings, + causing any action to automatically proceed. WARNING: + This bypasses confirmations for protective operations, + use with care.`, } skipUnlockFlag = &boolFlag{ Name: "skip-unlock", @@ -163,12 +157,35 @@ var ( } dropCachesFlag = &boolFlag{ Name: "drop-caches", - Usage: `After purging the keys from the keyring, drop the - associated caches for the purge to take effect. Without - this flag, cached encrypted files may still have their - plaintext visible. Requires root privileges.`, + Usage: `After removing the key(s) from the keyring, drop the + kernel's filesystem caches if needed. Without this flag, + files encrypted with v1 encryption policies may still be + accessible. This flag is not needed for v2 encryption + policies. This flag, if actually needed, requires root + privileges.`, Default: true, } + allUsersLockFlag = &boolFlag{ + Name: "all-users", + Usage: `Lock the directory no matter which user(s) have unlocked + it. Requires root privileges. This flag is only + necessary if the directory was unlocked by a user + different from the one you're locking it as. This flag + is only implemented for v2 encryption policies.`, + } + allUsersSetupFlag = &boolFlag{ + Name: "all-users", + Usage: `When setting up a filesystem for fscrypt, allow users + other than the calling user (typically root) to create + fscrypt policies and protectors on the filesystem. Note + that this will create a world-writable directory, which + users could use to fill up the entire filesystem. Hence, + this option may not be appropriate for some systems.`, + } + noRecoveryFlag = &boolFlag{ + Name: "no-recovery", + Usage: `Don't generate a recovery passphrase.`, + } ) // Option flags: used to specify options instead of being prompted for them @@ -205,12 +222,13 @@ var ( 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.`, + long. When running non-interactively and no key is provided, + will try to read the key from stdin.`, } userFlag = &stringFlag{ Name: "user", ArgName: "USERNAME", - Usage: `Specifiy which user should be used for login passphrases + Usage: `Specify which user should be used for login passphrases or to which user's keyring keys should be provisioned.`, } protectorFlag = &stringFlag{ @@ -255,18 +273,18 @@ func matchMetadataFlag(flagValue string) (mountpoint, descriptor string, err err // 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, target *user.User) (*actions.Context, string, error) { +func parseMetadataFlag(flagValue string, targetUser *user.User) (*actions.Context, string, error) { mountpoint, descriptor, err := matchMetadataFlag(flagValue) if err != nil { return nil, "", err } - ctx, err := actions.NewContextFromMountpoint(mountpoint, target) + ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser) return ctx, descriptor, err } // getProtectorFromFlag gets an existing locked protector from protectorFlag. -func getProtectorFromFlag(flagValue string, target *user.User) (*actions.Protector, error) { - ctx, descriptor, err := parseMetadataFlag(flagValue, target) +func getProtectorFromFlag(flagValue string, targetUser *user.User) (*actions.Protector, error) { + ctx, descriptor, err := parseMetadataFlag(flagValue, targetUser) if err != nil { return nil, err } @@ -274,8 +292,8 @@ func getProtectorFromFlag(flagValue string, target *user.User) (*actions.Protect } // getPolicyFromFlag gets an existing locked policy from policyFlag. -func getPolicyFromFlag(flagValue string, target *user.User) (*actions.Policy, error) { - ctx, descriptor, err := parseMetadataFlag(flagValue, target) +func getPolicyFromFlag(flagValue string, targetUser *user.User) (*actions.Policy, error) { + ctx, descriptor, err := parseMetadataFlag(flagValue, targetUser) if err != nil { return nil, err } @@ -283,24 +301,10 @@ func getPolicyFromFlag(flagValue string, target *user.User) (*actions.Policy, er } // parseUserFlag returns the user specified by userFlag or the current effective -// user if the flag value is missing. If the effective user is root, however, a -// user must specified in the flag. If checkKeyring is true, we also make sure -// there are no problems accessing the user keyring. -func parseUserFlag(checkKeyring bool) (targetUser *user.User, err error) { +// user if the flag value is missing. +func parseUserFlag() (targetUser *user.User, err error) { if userFlag.Value != "" { - targetUser, err = user.Lookup(userFlag.Value) - } else { - if util.IsUserRoot() { - return nil, ErrSpecifyUser - } - targetUser, err = util.EffectiveUser() - } - if err != nil { - return nil, err - } - - if checkKeyring { - _, err = security.UserKeyringID(targetUser, true) + return user.Lookup(userFlag.Value) } - return targetUser, err + return util.EffectiveUser() } diff --git a/cmd/fscrypt/format.go b/cmd/fscrypt/format.go index ef009d3..21253ad 100644 --- a/cmd/fscrypt/format.go +++ b/cmd/fscrypt/format.go @@ -25,12 +25,11 @@ import ( "bytes" "fmt" "os" - "regexp" "strings" "unicode/utf8" "github.com/urfave/cli" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" "github.com/google/fscrypt/util" ) @@ -65,8 +64,8 @@ func init() { 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 { + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || width <= 0 { lineLength = fallbackLineLength } else { lineLength = util.MinInt(width, maxLineLength) @@ -74,7 +73,7 @@ func init() { } -// Flags that conform to this interface can be used with an urfave/cli +// Flags that conform to this interface can be used with a urfave/cli // application and can be printed in the correct format. type prettyFlag interface { cli.Flag @@ -83,8 +82,10 @@ type prettyFlag interface { } // How a flag should appear on the command line. We have two formats: -// --name -// --name=ARG_NAME +// +// --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. @@ -97,22 +98,20 @@ func shortDisplay(f prettyFlag) string { // 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: +// --help Prints help screen for commands and subcommands. // -// --legacy Allow for support of older kernels with ext4 -// (before v4.8) and F2FS (before v4.6) filesystems. -// (default: true) +// If a default is specified, then it is appended to the usage. Example: // +// --time=TIME Calibrate passphrase hashing to take the +// specified amount of TIME (default: 1s) 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 + // We pad 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) @@ -120,41 +119,58 @@ func longDisplay(f prettyFlag, defaultString ...string) string { 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. +// contains a word which is too long, that word gets its own line. Paragraphs +// and "code blocks" are preserved. 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 + filled := 0 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. + + for _, line := range strings.Split(text, "\n") { + words := strings.Fields(line) + + // Preserve empty lines (paragraph separators). + if len(words) == 0 { + if filled != 0 { + buffer.WriteString("\n") + } buffer.WriteString("\n") - buffer.WriteString(delimiter) - buffer.WriteString(word) - spaceLeft = maxTextLen - wordLen - default: - // Write word on this line - buffer.WriteByte(' ') + filled = 0 + continue + } + + codeBlock := (words[0] == ">") + if codeBlock { + words[0] = " " + if filled != 0 { + buffer.WriteString("\n") + filled = 0 + } + } + for _, word := range words { + wordLen := utf8.RuneCountInString(word) + // Write a newline if needed. + if filled != 0 && filled+1+wordLen > lineLength && !codeBlock { + buffer.WriteString("\n") + filled = 0 + } + // Write a delimiter or space if needed. + if filled == 0 { + if buffer.Len() != 0 { + buffer.WriteString(delimiter) + } + filled += padding + } else { + buffer.WriteByte(' ') + filled++ + } + // Write the word. buffer.WriteString(word) - spaceLeft -= 1 + wordLen + filled += wordLen } } diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go index d6162f6..93f97de 100644 --- a/cmd/fscrypt/fscrypt.go +++ b/cmd/fscrypt/fscrypt.go @@ -22,54 +22,49 @@ fscrypt is a command line tool for managing linux filesystem encryption. */ -// +build linux,cgo - package main import ( "fmt" - "io/ioutil" + "io" "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", - }} + "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/filesystem" ) +// Current version of the program (set by Makefile) +var version string + func main() { cli.AppHelpTemplate = appHelpTemplate cli.CommandHelpTemplate = commandHelpTemplate cli.SubcommandHelpTemplate = subcommandHelpTemplate + if conffile := os.Getenv("FSCRYPT_CONF"); conffile != "" { + actions.ConfigFileLocation = conffile + } + if rootmnt := os.Getenv("FSCRYPT_ROOT_MNT"); rootmnt != "" { + actions.LoginProtectorMountpoint = rootmnt + } + if consistent := os.Getenv("FSCRYPT_CONSISTENT_OUTPUT"); consistent == "1" { + filesystem.SortDescriptorsByLastMtime = true + } + // 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. + // Grab the version 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 @@ -78,7 +73,7 @@ func main() { // Initialize command list and setup all of the commands. app.Action = defaultAction - app.Commands = []cli.Command{Setup, Encrypt, Unlock, Purge, Status, Metadata} + app.Commands = []cli.Command{Setup, Encrypt, Unlock, Lock, Purge, Status, Metadata} for i := range app.Commands { setupCommand(&app.Commands[i]) } @@ -87,7 +82,7 @@ func main() { } // setupCommand performs some common setup for each command. This includes -// hiding the help, formating the description, adding in the necessary +// hiding the help, formatting 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) { @@ -104,7 +99,7 @@ func setupCommand(command *cli.Command) { if len(command.Subcommands) == 0 { command.Before = setupBefore } else { - // Cleanup subcommands (if applicable) + // Setup subcommands (if applicable) for i := range command.Subcommands { setupCommand(&command.Subcommands[i]) } @@ -114,11 +109,9 @@ func setupCommand(command *cli.Command) { // setupBefore 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. -// When running with sudo, this function also verifies that we have the proper -// keyring linkage enabled. func setupBefore(c *cli.Context) error { - log.SetOutput(ioutil.Discard) - c.App.Writer = ioutil.Discard + log.SetOutput(io.Discard) + c.App.Writer = io.Discard if verboseFlag.Value { log.SetOutput(os.Stdout) diff --git a/cmd/fscrypt/fscrypt_bash_completion b/cmd/fscrypt/fscrypt_bash_completion new file mode 100644 index 0000000..110d2d4 --- /dev/null +++ b/cmd/fscrypt/fscrypt_bash_completion @@ -0,0 +1,332 @@ +# fscrypt_bash_completion +# +# Copyright 2017 Google Inc. +# Author: Henry-Joseph Audéoud (h.audeoud@gmail.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. + +# +# bash completion scripts require exercising some unusual shell script +# features/quirks, so we have to disable some shellcheck warnings: +# +# Disable SC2016 ("Expressions don't expand in single quotes, use double quotes +# for that") because the 'compgen' built-in expands the argument passed to -W, +# so that argument *must* be single-quoted to avoid command injection. +# shellcheck disable=SC2016 +# +# Disable SC2034 ("{Variable} appears unused. Verify use (or export if used +# externally)") because of the single quoting mentioned above as well as the +# fact that we have to declare "local" variables used only by a called function +# (_init_completion()) and not by the function itself. +# shellcheck disable=SC2034 +# +# Disable SC2207 ("Prefer mapfile or read -a to split command output (or quote +# to avoid splitting)") because bash completion scripts conventionally use +# COMPREPLY=($(...)) assignments. +# shellcheck disable=SC2207 +# +true # To apply the above shellcheck directives to the entire file + + +# Generate the completion list for possible mountpoints. +# +# We need to be super careful here because mountpoints can contain whitespace +# and shell meta-characters. To avoid most problems, we do the following: +# +# 1.) To avoid parsing ambiguities, 'fscrypt status' replaces the space, tab, +# newline, and backslash characters with octal escape sequences -- like +# what /proc/self/mountinfo does. To properly process its output, we need +# to split lines on space only (and not on other whitespace which might +# not be escaped), and unescape these characters. Exception: we don't +# unescape newlines, as we need to reserve newline as the separator for +# the words passed to compgen. (This causes mountpoints containing +# newlines to not be completed correctly, which we have to tolerate.) +# +# 2.) We backslash-escape all shell meta-characters, and single-quote the +# argument passed to compgen -W. Without either step, command injection +# would be possible. Without both steps, completions would be incorrect. +# The list of shell meta-characters used comes from that used by the +# completion script for umount, which has to solve this same problem. +# +_fscrypt_compgen_mountpoints() +{ + local IFS=$'\n' + compgen -W '$(_fscrypt_mountpoints_internal)' -- "${cur}" +} + +_fscrypt_mountpoints_internal() +{ + fscrypt status 2>/dev/null | command awk -F " " \ + 'substr($0, 1, 1) == "/" && $5 == "Yes" { + gsub(/\\040/, " ", $1) + gsub(/\\011/, "\t", $1) + gsub(/\\134/, "\\", $1) + gsub(/[\]\[(){}<>",:;^&!$=?`|'\''\\ \t\f\n\r\v]/, "\\\\&", $1) + print $1 + }' +} + +# Complete with all possible mountpoints +_fscrypt_complete_mountpoint() +{ + COMPREPLY=($(_fscrypt_compgen_mountpoints)) +} + + +# Output list of possible policy or protector IDs +# $1: the mount point on which policies are looked for. +# $2: the section (policy or protector) to retrieve +_fscrypt_status_section() +{ + local section=${2^^} + fscrypt status "$1" 2>/dev/null | \ + command awk '/^[[:xdigit:]]{16}/ && section == "'"$section"'" { print $1; next; } + { section = $1 }' +} + + +# Complete with policies or protectors +_fscrypt_complete_policy_or_protector() +{ + local status_section="$1" + if [[ $cur = *:* ]]; then + # Complete with IDs of the given mountpoint + local mountpoint="${cur%:*}" id="${cur#*:}" + # Note: compgen expands the argument to -W, so it *must* be single-quoted. + COMPREPLY=($(compgen \ + -W '$(_fscrypt_status_section "${mountpoint}" "${status_section}")' \ + -- "${id}")) + else + # Complete with mountpoints, with colon and without ending space + COMPREPLY=($(_fscrypt_compgen_mountpoints | sed s/\$/:/)) + compopt -o nospace + fi +} + + +# Complete with all arguments of that function +_fscrypt_complete_word() +{ + # Note: compgen expands the argument to -W, so it *must* be single-quoted. + COMPREPLY=($(compgen -W '$*' -- "${cur}")) +} + + +# Complete with all arguments of that function, plus global options +_fscrypt_complete_option() +{ + local additional_opts=( "$@" ) + # Add global options, always correct + additional_opts+=( --verbose --quiet --help ) + # Note: compgen expands the argument to -W, so it *must* be single-quoted. + COMPREPLY=($(compgen -W '${additional_opts[*]}' -- "${cur}")) +} + + +_fscrypt() +{ + # Initialize completion: compute some local variables to easily + # detect what is written on the command line. -s is for splitting + # long options on `=`, and -n is for splitting them also on `:` + # (used in the protectors/policies `MOUNTPOINT:ID` forms). + # + # `split` is set by `_init_completion -s`, we must declare it local + # even if we don't use it, not to modify the environment. + local cur prev words cword split + _init_completion -s -n : || return + + # Complete the options with argument here, if previous word were such + # an option. It would be too difficult to check if they take place in + # the correct command (such as `fscrypt status # --key ...`)—and that + # is the command's job—so just complete them first. + case $prev in + --key) + # Any file is accepted + _filedir + return ;; + --name) + # New value, nothing to complete + return ;; + --policy|--protector|--unlock-with) + local p_or_p="${prev#--}" + [[ $p_or_p = unlock-with ]] && p_or_p=protector + _fscrypt_complete_policy_or_protector "${p_or_p}" + return ;; + --source) + # Complete with keywords + _fscrypt_complete_word \ + pam_passphrase custom_passphrase raw_key + return ;; + --time) + # It's a time, hard to complete a number… + return ;; + --user) + # Complete with a user + COMPREPLY=($(compgen -u -- "${cur}")) + return ;; + esac + + # Fetch positional arguments (i.e. subcommands) + local positional + positional=() + local iword + for ((iword = 1; iword < ${#words[@]} - 1; iword++)); do + [[ ${words[iword - 1]} == --@(key|name|policy|protector|unlock-with|source|time|user) ]] \ + && continue # Argument of previous option, skip + [[ ${words[iword]} == -* ]] && continue # Option, skip + positional+=("${words[iword]}") + done + + # If completing the first positional, complete with all possible commands + if [[ ${#positional[@]} == 0 ]]; then + if [[ $cur == -* ]]; then + _fscrypt_complete_option + else + _fscrypt_complete_word \ + encrypt lock metadata purge setup status unlock + fi + return + fi + + # Complete according to that provided + case ${positional[0]-} in + encrypt) # Directory or option + if [[ $cur == -* ]]; then + _fscrypt_complete_option \ + --policy= --unlock-with= --protector= --source= \ + --user= --name= --key= --skip-unlock --no-recovery + else + _filedir -d + fi ;; + lock) # Directory or option + if [[ $cur == -* ]]; then + _fscrypt_complete_option --user= --all-users + else + _filedir -d + fi ;; + purge) # Mountpoint or options + if [[ $cur == -* ]]; then + _fscrypt_complete_option --user= --force + else + _fscrypt_complete_mountpoint + fi ;; + setup) # Mountpoint or options + if [[ $cur == -* ]]; then + _fscrypt_complete_option --time= --force + else + _fscrypt_complete_mountpoint + fi ;; + status) # Directory (only global options for this command) + if [[ $cur == -* ]]; then + _fscrypt_complete_option + else + _filedir -d + fi ;; + unlock) # Directory or option + if [[ $cur == -* ]]; then + _fscrypt_complete_option --unlock-with= --user= --key= + else + _filedir -d + fi ;; + metadata) + # This command has subcommands + if [[ ${#positional[@]} = 1 ]]; then + if [[ $cur = -* ]]; then + _fscrypt_complete_option + else + # Still no subcommand, complete with them + _fscrypt_complete_word \ + add-protector-to-policy create change-passphrase \ + destroy dump remove-protector-from-policy + fi + return + fi + # We have a subcommand, complete according to it + case ${positional[1]-} in + add-protector-to-policy) # Options only + _fscrypt_complete_option \ + --protector= --policy= --unlock-with= --key= + ;; + change-passphrase) # Options only + _fscrypt_complete_option --protector= + ;; + destroy) # Mountpoint or option + if [[ $cur == -* ]]; then + _fscrypt_complete_option \ + --protector= --policy= --force + else + _fscrypt_complete_mountpoint + fi ;; + dump) # Options only + _fscrypt_complete_option --protector= --policy= + ;; + remove-protector-from-policy) # Options only + _fscrypt_complete_option \ + --protector= --policy= --force + ;; + create) + # This subcommand has subsubcommands + if [[ ${#positional[@]} = 2 ]]; then + if [[ $cur = -* ]]; then + _fscrypt_complete_option + else + # Still no subcommand, complete with them + _fscrypt_complete_word protector policy + fi + return + fi + # We have a subsubcommand, complete according to it + case ${positional[2]-} in + policy) # Mountpoint or option + if [[ $cur = -* ]]; then + _fscrypt_complete_option --protector= --key= + else + _fscrypt_complete_mountpoint + fi ;; + protector) # Mountpoint or option + if [[ $cur = -* ]]; then + _fscrypt_complete_option \ + --source= --name= --key= --user= + else + _fscrypt_complete_mountpoint + fi ;; + *) + # Unrecognized subsubcommand… Suppose a new + # unknown subsubcommand and complete with + # global options only + _fscrypt_complete_option + ;; + esac + ;; + *) + # Unrecognized subcommand… Suppose a new unknown + # subcommand and complete with global options only + _fscrypt_complete_option + ;; + esac + ;; + *) + # Unrecognized command… Suppose a new unknown command and + # complete with global options only + _fscrypt_complete_option + ;; + esac + + # When the sole offered completion is --*=, do not put a space after + # the equal sign as we wait for the argument value. + [[ ${#COMPREPLY[@]} == 1 ]] && [[ ${COMPREPLY[0]} == "--"*"=" ]] \ + && compopt -o nospace +} && + complete -F _fscrypt fscrypt + +# ex: filetype=bash diff --git a/cmd/fscrypt/keys.go b/cmd/fscrypt/keys.go index 872ca2a..b57c01d 100644 --- a/cmd/fscrypt/keys.go +++ b/cmd/fscrypt/keys.go @@ -22,13 +22,14 @@ package main import ( + "bufio" "fmt" "io" "log" "os" "github.com/pkg/errors" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" "github.com/google/fscrypt/actions" "github.com/google/fscrypt/crypto" @@ -55,14 +56,14 @@ var ( // 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. +// Read gets input from the terminal until a newline is encountered or the given +// buffer is full. 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 + return position, nil } if _, err := io.ReadFull(os.Stdin, buf[position:position+1]); err != nil { return position, err @@ -86,25 +87,53 @@ func (p passphraseReader) Read(buf []byte) (int, error) { // 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.Print(prompt) - } // Only disable echo if stdin is actually a terminal. - if terminal.IsTerminal(stdinFd) { - state, err := terminal.MakeRaw(stdinFd) + if term.IsTerminal(stdinFd) { + state, err := term.MakeRaw(stdinFd) if err != nil { return nil, err } defer func() { - terminal.Restore(stdinFd, state) + term.Restore(stdinFd, state) fmt.Println() // To align input }() } + if !quietFlag.Value { + fmt.Print(prompt) + } + return crypto.NewKeyFromReader(passphraseReader{}) } +func makeRawKey(info actions.ProtectorInfo) (*crypto.Key, error) { + // When running non-interactively and no key was provided, + // try to read it from stdin + if keyFileFlag.Value == "" && !term.IsTerminal(stdinFd) { + return crypto.NewFixedLengthKeyFromReader(bufio.NewReader(os.Stdin), + metadata.InternalKeyLen) + } + + 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) +} + // 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). @@ -178,23 +207,7 @@ func makeKeyFunc(supportRetry, shouldConfirm bool, prefix string) actions.KeyFun 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) + return makeRawKey(info) default: return nil, ErrInvalidSource diff --git a/cmd/fscrypt/prompt.go b/cmd/fscrypt/prompt.go index 0031e8f..d34a18a 100644 --- a/cmd/fscrypt/prompt.go +++ b/cmd/fscrypt/prompt.go @@ -90,7 +90,7 @@ func askConfirmation(question string, defaultChoice bool, warning string) error // Defaults of "no" require forcing. if !defaultChoice { if quietFlag.Value { - return ErrNoDesctructiveOps + return ErrNoDestructiveOps } } @@ -185,7 +185,7 @@ func promptForSource(ctx *actions.Context) error { } // We print all the sources with their number, description, and name. - fmt.Println("Your data can be protected with one of the following sources:") + fmt.Println("The following protector sources are available:") for idx := 1; idx < len(metadata.SourceType_value); idx++ { source := metadata.SourceType(idx) description := sourceDescriptions[source] @@ -282,7 +282,8 @@ func promptForProtector(options []*actions.ProtectorOption) (int, error) { } if numLoadErrors > 0 { - fmt.Print(wrapText("NOTE: %d of the %d protectors failed to load. "+loadHelpText, 0)) + loadWarning := fmt.Sprintf("NOTE: %d of the %d protectors failed to load. ", numLoadErrors, numOptions) + fmt.Print(wrapText(loadWarning+loadHelpText, 0) + "\n") } for { @@ -318,7 +319,8 @@ func optionFn(policyDescriptor string, options []*actions.ProtectorOption) (int, return idx, nil } } - return 0, actions.ErrNotProtected + return 0, &actions.ErrNotProtected{PolicyDescriptor: policyDescriptor, + ProtectorDescriptor: protector.Descriptor()} } log.Printf("optionFn(%s)", policyDescriptor) diff --git a/cmd/fscrypt/protector.go b/cmd/fscrypt/protector.go index 32ba4ab..186ca7a 100644 --- a/cmd/fscrypt/protector.go +++ b/cmd/fscrypt/protector.go @@ -21,11 +21,14 @@ package main import ( + "fmt" "log" + "os/user" "github.com/google/fscrypt/actions" "github.com/google/fscrypt/filesystem" "github.com/google/fscrypt/metadata" + "github.com/google/fscrypt/util" ) // createProtector makes a new protector on either ctx.Mount or if the requested @@ -36,6 +39,19 @@ func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error return nil, err } log.Printf("using source: %s", ctx.Config.Source.String()) + if ctx.Config.Source == metadata.SourceType_pam_passphrase { + if userFlag.Value == "" && util.IsUserRoot() { + return nil, ErrSpecifyUser + } + if !quietFlag.Value { + fmt.Print(` +IMPORTANT: Before continuing, ensure you have properly set up your system for + login protectors. See + https://github.com/google/fscrypt#setting-up-for-login-protectors + +`) + } + } name, err := promptForName(ctx) if err != nil { @@ -45,18 +61,24 @@ func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error // 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.Config.Source == metadata.SourceType_pam_passphrase && + ctx.Mount.Path != actions.LoginProtectorMountpoint { + log.Printf("creating login protector on %q instead of %q", + actions.LoginProtectorMountpoint, ctx.Mount.Path) if ctx, err = modifiedContext(ctx); err != nil { return nil, err } } - return actions.CreateProtector(ctx, name, createKeyFn) + var owner *user.User + if ctx.Config.Source == metadata.SourceType_pam_passphrase && util.IsUserRoot() { + owner = ctx.TargetUser + } + return actions.CreateProtector(ctx, name, createKeyFn, owner) } // selectExistingProtector returns a locked Protector which corresponds to an -// options in the non-empty slice of options. Prompts for user input are used to +// option 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) @@ -78,7 +100,7 @@ func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption, } // Do nothing different if we are at the root, or cannot load the root. - if ctx.Mount.Path == "/" { + if ctx.Mount.Path == actions.LoginProtectorMountpoint { return options, nil } if ctx, err = modifiedContext(ctx); err != nil { @@ -111,10 +133,10 @@ func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption, return options, nil } -// modifiedContext returns a copy of ctx with the mountpoint replaced by that of -// the root filesystem. +// modifiedContext returns a copy of ctx with the mountpoint replaced by +// LoginProtectorMountpoint. func modifiedContext(ctx *actions.Context) (*actions.Context, error) { - mnt, err := filesystem.GetMount("/") + mnt, err := filesystem.GetMount(actions.LoginProtectorMountpoint) if err != nil { return nil, err } diff --git a/cmd/fscrypt/setup.go b/cmd/fscrypt/setup.go index 72dfbdb..b9a16e8 100644 --- a/cmd/fscrypt/setup.go +++ b/cmd/fscrypt/setup.go @@ -1,5 +1,5 @@ /* - * strings.go - File containing the functionality initializing directories and + * setup.go - File containing the functionality for initializing directories and * the global config file. * * Copyright 2017 Google Inc. @@ -26,6 +26,7 @@ import ( "os" "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/filesystem" "github.com/google/fscrypt/util" ) @@ -35,7 +36,7 @@ func createGlobalConfig(w io.Writer, path string) error { return ErrMustBeRoot } - // Ask to create or replace the config file + // If the config file already exists, ask to replace it _, err := os.Stat(path) switch { case err == nil: @@ -44,14 +45,28 @@ func createGlobalConfig(w io.Writer, path string) error { err = os.Remove(path) } case os.IsNotExist(err): - err = askConfirmation(fmt.Sprintf("Create %q?", path), true, "") + err = nil } if err != nil { return err } + // v2 encryption policies are recommended, so set policy_version 2 when + // the kernel supports it. v2 policies are supported by upstream Linux + // v5.4 and later. For now we simply check the kernel version. Ideally + // we'd instead check whether setting a v2 policy actually works, in + // order to also detect backports of the kernel patches. However, that's + // hard because from this context (creating /etc/fscrypt.conf) we may + // not yet have access to a filesystem that supports encryption. + var policyVersion int64 + if util.IsKernelVersionAtLeast(5, 4) { + fmt.Fprintln(w, "Defaulting to policy_version 2 because kernel supports it.") + policyVersion = 2 + } else { + fmt.Fprintln(w, "Defaulting to policy_version 1 because kernel doesn't support v2.") + } fmt.Fprintln(w, "Customizing passphrase hashing difficulty for this system...") - err = actions.CreateConfigFile(timeTargetFlag.Value, legacyFlag.Value) + err = actions.CreateConfigFile(timeTargetFlag.Value, policyVersion) if err != nil { return err } @@ -66,13 +81,47 @@ func setupFilesystem(w io.Writer, path string) error { if err != nil { return err } + username := ctx.TargetUser.Username + + err = ctx.Mount.CheckSetup(ctx.TrustedUser) + if err == nil { + return &filesystem.ErrAlreadySetup{Mount: ctx.Mount} + } + if _, ok := err.(*filesystem.ErrNotSetup); !ok { + return err + } - if err = ctx.Mount.Setup(); err != nil { + allUsers := allUsersSetupFlag.Value + if !allUsers { + thisFilesystem := "this filesystem" + if ctx.Mount.Path == "/" { + thisFilesystem = "the root filesystem" + } + prompt := fmt.Sprintf(`Allow users other than %s to create +fscrypt metadata on %s? (See +https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem)`, + username, thisFilesystem) + allUsers, err = askQuestion(wrapText(prompt, 0), false) + if err != nil { + return err + } + } + var setupMode filesystem.SetupMode + if allUsers { + setupMode = filesystem.WorldWritable + } else { + setupMode = filesystem.SingleUserWritable + } + if err = ctx.Mount.Setup(setupMode); 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) + if allUsers { + fmt.Fprintf(w, "Metadata directories created at %q, writable by everyone.\n", + ctx.Mount.BaseDir()) + } else { + fmt.Fprintf(w, "Metadata directories created at %q, writable by %s only.\n", + ctx.Mount.BaseDir(), username) + } return nil } diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go index 1465a4e..bc8f1ee 100644 --- a/cmd/fscrypt/status.go +++ b/cmd/fscrypt/status.go @@ -27,11 +27,9 @@ import ( "strings" "text/tabwriter" - "github.com/pkg/errors" - "github.com/google/fscrypt/actions" "github.com/google/fscrypt/filesystem" - "github.com/google/fscrypt/metadata" + "github.com/google/fscrypt/keyring" ) // Creates a writer which correctly aligns tabs with the specified header. @@ -45,12 +43,13 @@ func makeTableWriter(w io.Writer, header string) *tabwriter.Writer { // encryptionStatus will be printed in the ENCRYPTION column. An empty string // indicates the filesystem should not be printed. func encryptionStatus(err error) string { - switch errors.Cause(err) { - case nil: + if err == nil { return "supported" - case metadata.ErrEncryptionNotEnabled: + } + switch err.(type) { + case *filesystem.ErrEncryptionNotEnabled: return "not enabled" - case metadata.ErrEncryptionNotSupported: + case *filesystem.ErrEncryptionNotSupported: return "not supported" default: // Unknown error regarding support @@ -65,7 +64,31 @@ func yesNoString(b bool) string { return "No" } -// writeGlobalStatus prints all the filesystem that use (or could use) fscrypt. +func policyUnlockedStatus(policy *actions.Policy, path string) string { + status := policy.GetProvisioningStatus() + + // Due to a limitation in the old kernel API for fscrypt, for v1 + // policies using the user keyring that are incompletely locked or are + // unlocked by another user, we'll get KeyAbsent. If we have a + // directory path, use a heuristic to try to detect these cases. + if status == keyring.KeyAbsent && policy.NeedsUserKeyring() && + path != "" && isDirUnlockedHeuristic(path) { + return "Partially (incompletely locked, or unlocked by another user)" + } + + switch status { + case keyring.KeyPresent, keyring.KeyPresentButOnlyOtherUsers: + return "Yes" + case keyring.KeyAbsent: + return "No" + case keyring.KeyAbsentButFilesBusy: + return "Partially (incompletely locked)" + default: + return "Unknown" + } +} + +// writeGlobalStatus prints all the filesystems that use (or could use) fscrypt. func writeGlobalStatus(w io.Writer) error { mounts, err := filesystem.AllFilesystems() if err != nil { @@ -78,7 +101,7 @@ func writeGlobalStatus(w io.Writer) error { t := makeTableWriter(w, "MOUNTPOINT\tDEVICE\tFILESYSTEM\tENCRYPTION\tFSCRYPT") for _, mount := range mounts { // Only print mountpoints backed by devices or using fscrypt. - usingFscrypt := mount.CheckSetup() == nil + usingFscrypt := mount.CheckSetup(nil) == nil if !usingFscrypt && mount.Device == "" { continue } @@ -91,7 +114,10 @@ func writeGlobalStatus(w io.Writer) error { continue } - fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\n", mount.Path, mount.Device, mount.Filesystem, + fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\n", + filesystem.EscapeString(mount.Path), + filesystem.EscapeString(mount.Device), + filesystem.EscapeString(mount.FilesystemType), supportString, yesNoString(usingFscrypt)) if supportErr == nil { @@ -134,13 +160,27 @@ func writeFilesystemStatus(w io.Writer, ctx *actions.Context) error { return err } - policyDescriptors, err := ctx.Mount.ListPolicies() + policyDescriptors, err := ctx.Mount.ListPolicies(ctx.TrustedUser) if err != nil { return err } - fmt.Fprintf(w, "%s filesystem %q has %s and %s\n\n", ctx.Mount.Filesystem, ctx.Mount.Path, - pluralize(len(options), "protector"), pluralize(len(policyDescriptors), "policy")) + filterDescription := "" + if ctx.TrustedUser != nil { + filterDescription = fmt.Sprintf(" (only including ones owned by %s or root)", ctx.TrustedUser.Username) + } + fmt.Fprintf(w, "%s filesystem %q has %s and %s%s.\n", ctx.Mount.FilesystemType, + ctx.Mount.Path, pluralize(len(options), "protector"), + pluralize(len(policyDescriptors), "policy"), filterDescription) + if setupMode, user, err := ctx.Mount.GetSetupMode(); err == nil { + switch setupMode { + case filesystem.WorldWritable: + fmt.Fprintf(w, "All users can create fscrypt metadata on this filesystem.\n") + case filesystem.SingleUserWritable: + fmt.Fprintf(w, "Only %s can create fscrypt metadata on this filesystem.\n", user.Username) + } + } + fmt.Fprintf(w, "\n") if len(options) > 0 { writeOptions(w, options) @@ -159,7 +199,8 @@ func writeFilesystemStatus(w io.Writer, ctx *actions.Context) error { continue } - fmt.Fprintf(t, "%s\t%s\t%s\n", descriptor, yesNoString(policy.IsProvisioned()), + fmt.Fprintf(t, "%s\t%s\t%s\n", descriptor, + policyUnlockedStatus(policy, ""), strings.Join(policy.ProtectorDescriptors(), ", ")) } return t.Flush() @@ -178,7 +219,8 @@ func writePathStatus(w io.Writer, path string) error { fmt.Fprintf(w, "%q is encrypted with fscrypt.\n", path) fmt.Fprintln(w) fmt.Fprintf(w, "Policy: %s\n", policy.Descriptor()) - fmt.Fprintf(w, "Unlocked: %s\n", yesNoString(policy.IsProvisioned())) + fmt.Fprintf(w, "Options: %s\n", policy.Options()) + fmt.Fprintf(w, "Unlocked: %s\n", policyUnlockedStatus(policy, path)) fmt.Fprintln(w) options := policy.ProtectorOptions() diff --git a/cmd/fscrypt/strings.go b/cmd/fscrypt/strings.go index fb79c38..cd51968 100644 --- a/cmd/fscrypt/strings.go +++ b/cmd/fscrypt/strings.go @@ -25,24 +25,7 @@ import ( "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.` -) +const shortUsage = "A tool for managing Linux native filesystem encryption" // Argument usage strings const ( @@ -103,23 +86,6 @@ 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. @@ -130,7 +96,7 @@ var plurals = map[string]string{ "policy": "policies", } -// pluralize prints our the correct pluralization of a work along with the +// pluralize prints out the correct pluralization of a word 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 { |