From de51add609bc74b7247ec4776bd694abbea24a45 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sat, 9 May 2020 14:17:17 -0700 Subject: Try to detect incomplete locking of v1-encrypted directory 'fscrypt lock' on a v1-encrypted directory doesn't warn about in-use files, as the kernel doesn't provide a way to easily detect it. Instead, implement a heuristic where we check whether a subdirectory can be created. If yes, then the directory must not be fully locked. Make both 'fscrypt lock' and 'fscrypt status' use this heuristic. Resolves https://github.com/google/fscrypt/issues/215 --- cmd/fscrypt/commands.go | 39 ++++++++++++++++++++++++++++++++------- cmd/fscrypt/status.go | 21 +++++++++++++++++---- 2 files changed, 49 insertions(+), 11 deletions(-) (limited to 'cmd') diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index ec75584..51cf136 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -496,30 +496,55 @@ func lockAction(c *cli.Context) error { 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()) - return newExitError(c, errors.Wrapf(ErrPolicyLocked, path)) - } - // Check for permission to drop caches, if it will be needed. + // 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(allUsersFlag.Value); err != nil { - return newExitError(c, err) + if err != keyring.ErrKeyNotPresent { + 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.Wrapf(ErrPolicyLocked, path)) + } } if policy.NeedsUserKeyring() { if err = dropCachesIfRequested(c, ctx); err != nil { return newExitError(c, err) } + if isDirUnlockedHeuristic(path) { + return newExitError(c, keyring.ErrKeyFilesOpen) + } } fmt.Fprintf(c.App.Writer, "%q is now locked.\n", path) return nil } +// isDirUnlockedHeuristic returns true if we can create a subdirectory of the +// given directory and therefore it is definitely still unlocked. 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 + } + return false +} + // Purge removes all the policy keys from the keyring (also need unmount). var Purge = cli.Command{ Name: "purge", diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go index bf11495..40bb49e 100644 --- a/cmd/fscrypt/status.go +++ b/cmd/fscrypt/status.go @@ -66,8 +66,20 @@ func yesNoString(b bool) string { return "No" } -func policyUnlockedStatus(policy *actions.Policy) string { - switch policy.GetProvisioningStatus() { +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 we'll + // get KeyAbsent, not KeyAbsentButFilesBusy as expected. If we have a + // directory path, use a heuristic to try to detect whether it is still + // usable and thus the policy is actually incompletely locked. + if status == keyring.KeyAbsent && policy.NeedsUserKeyring() && + path != "" && isDirUnlockedHeuristic(path) { + status = keyring.KeyAbsentButFilesBusy + } + + switch status { case keyring.KeyPresent, keyring.KeyPresentButOnlyOtherUsers: return "Yes" case keyring.KeyAbsent: @@ -174,7 +186,8 @@ func writeFilesystemStatus(w io.Writer, ctx *actions.Context) error { continue } - fmt.Fprintf(t, "%s\t%s\t%s\n", descriptor, policyUnlockedStatus(policy), + fmt.Fprintf(t, "%s\t%s\t%s\n", descriptor, + policyUnlockedStatus(policy, ""), strings.Join(policy.ProtectorDescriptors(), ", ")) } return t.Flush() @@ -194,7 +207,7 @@ func writePathStatus(w io.Writer, path string) error { fmt.Fprintln(w) fmt.Fprintf(w, "Policy: %s\n", policy.Descriptor()) fmt.Fprintf(w, "Options: %s\n", policy.Options()) - fmt.Fprintf(w, "Unlocked: %s\n", policyUnlockedStatus(policy)) + fmt.Fprintf(w, "Unlocked: %s\n", policyUnlockedStatus(policy, path)) fmt.Fprintln(w) options := policy.ProtectorOptions() -- cgit v1.2.3