From 9003a0331a112e8901fae8279f4897a825ee8069 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sun, 15 Dec 2019 19:31:39 -0800 Subject: cmd/fscrypt: add 'fscrypt lock' command Add support for 'fscrypt lock'. This command "locks" a directory, undoing 'fscrypt unlock'. When the filesystem keyring is used, 'fscrypt lock' also detects when a directory wasn't fully locked due to some files still being in-use. It can then be run again later to try to finish locking the files. --- README.md | 15 ++++---- actions/policy.go | 6 ++++ cmd/fscrypt/commands.go | 95 ++++++++++++++++++++++++++++++++++++++++++++----- cmd/fscrypt/errors.go | 6 ++++ cmd/fscrypt/fscrypt.go | 2 +- 5 files changed, 106 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3a86723..f183f2e 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,8 @@ Concretely, fscrypt contains the following functionality: * `fscrypt setup MOUNTPOINT` - Gets a filesystem ready for use with fscrypt * `fscrypt encrypt DIRECTORY` - Encrypts an empty directory * `fscrypt unlock DIRECTORY` - Unlocks an encrypted directory -* `fscrypt purge MOUNTPOINT` - Removes keys for a filesystem before unmounting +* `fscrypt lock DIRECTORY` - Locks an encrypted directory +* `fscrypt purge MOUNTPOINT` - Locks all encrypted directories on a filesystem * `fscrypt status [PATH]` - Gets detailed info about filesystems or paths * `fscrypt metadata` - Manages policies or protectors directly @@ -367,12 +368,10 @@ Protected with 1 protector: PROTECTOR LINKED DESCRIPTION 7626382168311a9d No custom protector "Super Secret" -# Purging a filesystem locks all the files. ->>>>> sudo fscrypt purge /mnt/disk --user=$USER -WARNING: Encrypted data on this filesystem will be inaccessible until unlocked again!! -Purge all policy keys from "/mnt/disk" and drop global inode cache? [y/N] y -Policies purged for "/mnt/disk". - +# Lock the directory. +>>>>> sudo fscrypt lock /mnt/disk/dir1 --user=$USER +Encrypted data removed from filesystem cache. +"/mnt/disk/dir1" is now locked. >>>>> fscrypt status /mnt/disk/dir1 "/mnt/disk/dir1" is encrypted with fscrypt. @@ -410,7 +409,7 @@ Hello World #### Quiet Version ```bash ->>>>> sudo fscrypt purge /mnt/disk --user=$USER --quiet --force +>>>>> sudo fscrypt lock /mnt/disk/dir1 --quiet --user=$USER >>>>> echo "hunter2" | fscrypt unlock /mnt/disk/dir1 --quiet ``` diff --git a/actions/policy.go b/actions/policy.go index 6ef83ce..2d8c521 100644 --- a/actions/policy.go +++ b/actions/policy.go @@ -406,6 +406,12 @@ func (policy *Policy) Deprovision() error { policy.Context.getKeyringOptions()) } +// NeedsUserKeyring returns true if Provision and Deprovision for this policy +// will use a user keyring, not a filesystem keyring. +func (policy *Policy) NeedsUserKeyring() bool { + return !policy.Context.Config.GetUseFsKeyringForV1Policies() +} + // commitData writes the Policy's current data to the filesystem. func (policy *Policy) commitData() error { return policy.Context.Mount.AddPolicy(policy.data) diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index a3bfef2..8f2d21b 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -281,8 +281,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, @@ -328,6 +328,88 @@ 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. + + The %s=true option may be needed to properly lock the directory. + Root is required for this. + + 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}, + Action: lockAction, +} + +func lockAction(c *cli.Context) error { + if c.NArg() != 1 { + return expectedArgsErr(c, 1, false) + } + + targetUser, err := parseUserFlag(true) + 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) + } + // Check if directory is already locked + if policy.IsFullyDeprovisioned() { + log.Printf("policy %s is already fully deprovisioned", policy.Descriptor()) + return newExitError(c, errors.Wrapf(ErrPolicyLocked, path)) + } + // Check for permission to drop caches, if it will be needed. + if policy.NeedsUserKeyring() && dropCachesFlag.Value && !util.IsUserRoot() { + return newExitError(c, ErrDropCachesPerm) + } + + if err = policy.Deprovision(); err != nil { + return newExitError(c, err) + } + + if policy.NeedsUserKeyring() { + if err = dropCachesIfRequested(c, ctx); err != nil { + return newExitError(c, err) + } + } + + fmt.Fprintf(c.App.Writer, "%q is now locked.\n", path) + return nil +} + // Purge removes all the policy keys from the keyring (also need unmount). var Purge = cli.Command{ Name: "purge", @@ -401,13 +483,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 from 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 } diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go index ed57dbe..68135fe 100644 --- a/cmd/fscrypt/errors.go +++ b/cmd/fscrypt/errors.go @@ -56,6 +56,7 @@ var ( 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") + ErrPolicyLocked = errors.New("this file or directory is already locked") 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") @@ -94,6 +95,11 @@ func getErrorSuggestions(err error) string { 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 keyring.ErrKeyFilesOpen: + return `Directory was incompletely locked because some files are + still open. These files remain accessible. Try killing + any processes using files in the directory, then + re-running 'fscrypt lock'.` case keyring.ErrSessionUserKeying: return `This is usually the result of a bad PAM configuration. Either correct the problem in your PAM stack, enable diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go index 9ac8e2f..b6549f4 100644 --- a/cmd/fscrypt/fscrypt.go +++ b/cmd/fscrypt/fscrypt.go @@ -76,7 +76,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]) } -- cgit v1.2.3