Skip to content

Commit

Permalink
Try to detect incomplete locking of v1-encrypted directory
Browse files Browse the repository at this point in the history
'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 #215
  • Loading branch information
ebiggers committed Apr 19, 2020
1 parent fe86093 commit 7106410
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 19 deletions.
11 changes: 3 additions & 8 deletions actions/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,6 @@ func (policy *Policy) IsProvisionedByTargetUser() bool {
return policy.GetProvisioningStatus() == keyring.KeyPresent
}

// IsFullyDeprovisioned returns true if the policy has been fully deprovisioned,
// including by all users and with all files protected by it having been closed.
func (policy *Policy) IsFullyDeprovisioned() bool {
return policy.GetProvisioningStatus() == keyring.KeyAbsent
}

// Provision inserts the Policy key into the kernel keyring. This allows reading
// and writing of files encrypted with this directory. Requires unlocked Policy.
func (policy *Policy) Provision() error {
Expand All @@ -435,14 +429,15 @@ func (policy *Policy) Provision() error {

// Deprovision removes the Policy key from the kernel keyring. This prevents
// reading and writing to the directory --- unless the target keyring is a user
// keyring, in which case caches must be dropped too.
// keyring, in which case caches must be dropped too. If the Policy key was
// already removed, returns keyring.ErrKeyNotPresent.
func (policy *Policy) Deprovision(allUsers bool) error {
return keyring.RemoveEncryptionKey(policy.Descriptor(),
policy.Context.getKeyringOptions(), allUsers)
}

// NeedsUserKeyring returns true if Provision and Deprovision for this policy
// will use a user keyring, not a filesystem keyring.
// will use a user keyring (deprecated), not a filesystem keyring.
func (policy *Policy) NeedsUserKeyring() bool {
return policy.Version() == 1 && !policy.Context.Config.GetUseFsKeyringForV1Policies()
}
Expand Down
39 changes: 32 additions & 7 deletions cmd/fscrypt/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,30 +495,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",
Expand Down
21 changes: 17 additions & 4 deletions cmd/fscrypt/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down

0 comments on commit 7106410

Please sign in to comment.