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. --- cmd/fscrypt/commands.go | 95 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 9 deletions(-) (limited to 'cmd/fscrypt/commands.go') 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 } -- cgit v1.2.3 From 0829eb74863bd279ae012779e52040ecc7f7178e Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sun, 15 Dec 2019 19:31:39 -0800 Subject: cmd/fscrypt: adjust user and keyring validation and preparation Don't force the user to provide a --user argument when running fscrypt as root if they're doing something where the TargetUser isn't actually needed, such as provisioning/deprovisioning a v1 encryption policy to/from the filesystem keyring, or creating a non-login protector. Also don't set up the user keyring (or check for it being set up) if it won't actually be used. Finally, if we'll be provisioning/deprovisioning a v1 encryption policy to/from the filesystem keyring, make sure the command is running as root, since the kernel requires this. --- cmd/fscrypt/commands.go | 69 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 10 deletions(-) (limited to 'cmd/fscrypt/commands.go') diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index 8f2d21b..621651e 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -30,6 +30,7 @@ import ( "github.com/google/fscrypt/actions" "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" @@ -134,11 +135,35 @@ 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 { + 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 +} + // 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) { - targetUser, err := parseUserFlag(!skipUnlockFlag.Value) + targetUser, err := parseUserFlag() if err != nil { return } @@ -154,10 +179,24 @@ func encryptPath(path string) (err error) { 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 + } + + 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 { @@ -173,12 +212,11 @@ func encryptPath(path string) (err error) { if err = protector.Unlock(existingKeyFn); err != nil { return } - policy, err = actions.CreatePolicy(ctx, protector) + if policy, err = actions.CreatePolicy(ctx, protector); err != nil { + return + } } // Successfully created policy should be reverted on failure. - if err != nil { - return - } defer func() { policy.Lock() if err != nil { @@ -293,7 +331,7 @@ func unlockAction(c *cli.Context) error { return expectedArgsErr(c, 1, false) } - targetUser, err := parseUserFlag(true) + targetUser, err := parseUserFlag() if err != nil { return newExitError(c, err) } @@ -309,6 +347,10 @@ 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()) @@ -370,7 +412,7 @@ func lockAction(c *cli.Context) error { return expectedArgsErr(c, 1, false) } - targetUser, err := parseUserFlag(true) + targetUser, err := parseUserFlag() if err != nil { return newExitError(c, err) } @@ -386,6 +428,10 @@ func lockAction(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 locked if policy.IsFullyDeprovisioned() { log.Printf("policy %s is already fully deprovisioned", policy.Descriptor()) @@ -459,7 +505,7 @@ func purgeAction(c *cli.Context) error { } } - targetUser, err := parseUserFlag(true) + targetUser, err := parseUserFlag() if err != nil { return newExitError(c, err) } @@ -468,6 +514,9 @@ func purgeAction(c *cli.Context) error { 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 { @@ -604,7 +653,7 @@ func createProtectorAction(c *cli.Context) error { return expectedArgsErr(c, 1, false) } - targetUser, err := parseUserFlag(false) + targetUser, err := parseUserFlag() if err != nil { return newExitError(c, err) } -- cgit v1.2.3 From 42e0dfe85ec7a75a2fa30c417d57eae60b5a881d Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sun, 15 Dec 2019 19:31:39 -0800 Subject: Keyring support for v2 encryption policies Implement adding/removing v2 encryption policy keys to/from the kernel. The kernel requires that the new ioctls FS_IOC_ADD_ENCRYPTION_KEY and FS_IOC_REMOVE_ENCRYPTION_KEY be used for this. Root is not required. However, non-root support brings an extra complication: the kernel keeps track of which users have called FS_IOC_ADD_ENCRYPTION_KEY for the same key. FS_IOC_REMOVE_ENCRYPTION_KEY only works as one of these users, and it only removes the calling user's claim to the key; the key is only truly removed when the last claim is removed. Implement the following behavior: - 'fscrypt unlock' and pam_fscrypt add the key for the user, even if other user(s) have it added already. This behavior is needed so that another user can't remove the key out from under the user. - 'fscrypt lock' and pam_fscrypt remove the key for the user. However, if the key wasn't truly removed because other users still have it added, 'fscrypt lock' prints a warning. - 'fscrypt status' shows whether the directory is unlocked for anyone. --- cmd/fscrypt/commands.go | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) (limited to 'cmd/fscrypt/commands.go') diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index 621651e..0bf0a4c 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -139,6 +139,18 @@ func encryptAction(c *cli.Context) error { // 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. @@ -225,14 +237,19 @@ func encryptPath(path string) (err error) { } }() - // Unlock() first, so if the Unlock() fails the directory isn't changed. - if !skipUnlockFlag.Value { + // 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 } + if skipUnlockFlag.Value { + defer policy.Deprovision() + } } if err = policy.Apply(path); os.IsPermission(errors.Cause(err)) { // EACCES at this point indicates ownership issues. @@ -352,8 +369,9 @@ func unlockAction(c *cli.Context) error { return newExitError(c, err) } // Check if directory is already unlocked - if policy.IsProvisioned() { - log.Printf("policy %s is already provisioned", policy.Descriptor()) + if policy.IsProvisionedByTargetUser() { + log.Printf("policy %s is already provisioned by %v", + policy.Descriptor(), ctx.TargetUser.Username) return newExitError(c, errors.Wrapf(ErrPolicyUnlocked, path)) } @@ -395,8 +413,13 @@ var Lock = cli.Command{ 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. + 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 -- cgit v1.2.3 From 068879664efd8a0f983cbc3e8115571047fe9edd Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sun, 15 Dec 2019 19:31:39 -0800 Subject: cmd/fscrypt, keyring: add --all-users option to 'fscrypt lock' Allow root to provide the --all-users option to 'fscrypt lock' to force an encryption key to be removed from the filesystem (i.e., force an encrypted directory to be locked), even if other users have added it. To implement this option, we just need to use the FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS ioctl rather than FS_IOC_REMOVE_ENCRYPTION_KEY. In theory this option could be implemented for the user keyrings case too, but it would be difficult and the user keyrings are being deprecated for fscrypt, so don't bother. --- cmd/fscrypt/commands.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'cmd/fscrypt/commands.go') diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index 0bf0a4c..41009b0 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -232,7 +232,7 @@ func encryptPath(path string) (err error) { defer func() { policy.Lock() if err != nil { - policy.Deprovision() + policy.Deprovision(false) policy.Revert() } }() @@ -248,7 +248,7 @@ func encryptPath(path string) (err error) { return } if skipUnlockFlag.Value { - defer policy.Deprovision() + defer policy.Deprovision(false) } } if err = policy.Apply(path); os.IsPermission(errors.Cause(err)) { @@ -426,7 +426,7 @@ var Lock = cli.Command{ 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}, + Flags: []cli.Flag{dropCachesFlag, userFlag, allUsersFlag}, Action: lockAction, } @@ -465,7 +465,7 @@ func lockAction(c *cli.Context) error { return newExitError(c, ErrDropCachesPerm) } - if err = policy.Deprovision(); err != nil { + if err = policy.Deprovision(allUsersFlag.Value); err != nil { return newExitError(c, err) } -- cgit v1.2.3