aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorebiggers <ebiggers@google.com>2020-01-22 18:28:23 -0800
committerGitHub <noreply@github.com>2020-01-22 18:28:23 -0800
commit059482129c5fdafebc582887a4ae4ef80988b708 (patch)
tree8ec373c41a677ff6949148b56f4aeaafe22791a6 /cmd
parent80654f23ebfd552277ed217a2c5e1d0bb1374189 (diff)
parentfe2939cc7e50f4c6025253efdf7380c04fac9ae1 (diff)
Merge pull request #148 from ebiggers/fscrypt-key-mgmt-improvements
Filesystem keyring and v2 encryption policy support
Diffstat (limited to 'cmd')
-rw-r--r--cmd/fscrypt/commands.go195
-rw-r--r--cmd/fscrypt/errors.go24
-rw-r--r--cmd/fscrypt/flags.go43
-rw-r--r--cmd/fscrypt/fscrypt.go2
-rw-r--r--cmd/fscrypt/protector.go6
-rw-r--r--cmd/fscrypt/status.go18
6 files changed, 235 insertions, 53 deletions
diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go
index a3bfef2..41009b0 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,47 @@ 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
+}
+
// 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 +191,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,28 +224,32 @@ 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 {
- policy.Deprovision()
+ policy.Deprovision(false)
policy.Revert()
}
}()
- // 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(false)
+ }
}
if err = policy.Apply(path); os.IsPermission(errors.Cause(err)) {
// EACCES at this point indicates ownership issues.
@@ -281,8 +336,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,
@@ -293,7 +348,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,9 +364,14 @@ 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())
+ if policy.IsProvisionedByTargetUser() {
+ log.Printf("policy %s is already provisioned by %v",
+ policy.Descriptor(), ctx.TargetUser.Username)
return newExitError(c, errors.Wrapf(ErrPolicyUnlocked, path))
}
@@ -328,6 +388,97 @@ 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, allUsersFlag},
+ 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 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(allUsersFlag.Value); 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",
@@ -377,7 +528,7 @@ func purgeAction(c *cli.Context) error {
}
}
- targetUser, err := parseUserFlag(true)
+ targetUser, err := parseUserFlag()
if err != nil {
return newExitError(c, err)
}
@@ -386,6 +537,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 {
@@ -401,13 +555,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
}
@@ -527,7 +676,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)
}
diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go
index 288e697..5239155 100644
--- a/cmd/fscrypt/errors.go
+++ b/cmd/fscrypt/errors.go
@@ -34,8 +34,8 @@ import (
"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"
)
@@ -56,12 +56,14 @@ 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")
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")
)
var loadHelpText = fmt.Sprintf("You may need to mount a linked filesystem. Run with %s for more information.", shortDisplay(verboseFlag))
@@ -94,11 +96,20 @@ 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 security.ErrSessionUserKeying:
+ 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.ErrKeyAddedByOtherUsers:
+ return `Directory couldn't be fully locked because other user(s)
+ have unlocked it. If you want to force the directory to
+ be locked, use 'sudo fscrypt lock --all-users DIR'.`
+ case keyring.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:
+ 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))
@@ -135,6 +146,13 @@ func getErrorSuggestions(err error) string {
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
diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go
index 16a75dc..b7933c9 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"
)
@@ -117,7 +116,7 @@ var (
allFlags = []prettyFlag{helpFlag, versionFlag, verboseFlag, quietFlag,
forceFlag, legacyFlag, skipUnlockFlag, timeTargetFlag,
sourceFlag, nameFlag, keyFileFlag, protectorFlag,
- unlockWithFlag, policyFlag}
+ unlockWithFlag, policyFlag, allUsersFlag}
// universalFlags contains flags that should be on every command
universalFlags = []cli.Flag{verboseFlag, quietFlag, helpFlag}
)
@@ -163,12 +162,22 @@ 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,
}
+ allUsersFlag = &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.`,
+ }
)
// Option flags: used to specify options instead of being prompted for them
@@ -283,24 +292,10 @@ func getPolicyFromFlag(flagValue string, targetUser *user.User) (*actions.Policy
}
// 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/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])
}
diff --git a/cmd/fscrypt/protector.go b/cmd/fscrypt/protector.go
index 8cbcf03..25f1984 100644
--- a/cmd/fscrypt/protector.go
+++ b/cmd/fscrypt/protector.go
@@ -26,6 +26,7 @@ import (
"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
@@ -37,6 +38,11 @@ func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error
}
log.Printf("using source: %s", ctx.Config.Source.String())
+ if ctx.Config.Source == metadata.SourceType_pam_passphrase &&
+ userFlag.Value == "" && util.IsUserRoot() {
+ return nil, ErrSpecifyUser
+ }
+
name, err := promptForName(ctx)
if err != nil {
return nil, err
diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go
index 375899b..bf11495 100644
--- a/cmd/fscrypt/status.go
+++ b/cmd/fscrypt/status.go
@@ -31,6 +31,7 @@ import (
"github.com/google/fscrypt/actions"
"github.com/google/fscrypt/filesystem"
+ "github.com/google/fscrypt/keyring"
"github.com/google/fscrypt/metadata"
)
@@ -65,6 +66,19 @@ func yesNoString(b bool) string {
return "No"
}
+func policyUnlockedStatus(policy *actions.Policy) string {
+ switch policy.GetProvisioningStatus() {
+ 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()
@@ -160,7 +174,7 @@ 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()
@@ -180,7 +194,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", yesNoString(policy.IsProvisioned()))
+ fmt.Fprintf(w, "Unlocked: %s\n", policyUnlockedStatus(policy))
fmt.Fprintln(w)
options := policy.ProtectorOptions()