aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/fscrypt/commands.go482
-rw-r--r--cmd/fscrypt/errors.go267
-rw-r--r--cmd/fscrypt/flags.go92
-rw-r--r--cmd/fscrypt/format.go98
-rw-r--r--cmd/fscrypt/fscrypt.go51
-rw-r--r--cmd/fscrypt/fscrypt_bash_completion332
-rw-r--r--cmd/fscrypt/keys.go67
-rw-r--r--cmd/fscrypt/prompt.go10
-rw-r--r--cmd/fscrypt/protector.go38
-rw-r--r--cmd/fscrypt/setup.go65
-rw-r--r--cmd/fscrypt/status.go72
-rw-r--r--cmd/fscrypt/strings.go38
12 files changed, 1241 insertions, 371 deletions
diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go
index 2f23a0f..3fc68a9 100644
--- a/cmd/fscrypt/commands.go
+++ b/cmd/fscrypt/commands.go
@@ -24,55 +24,72 @@ import (
"fmt"
"log"
"os"
+ "path/filepath"
+ "strings"
"github.com/pkg/errors"
"github.com/urfave/cli"
"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"
)
-// Setup is a command which can to global or per-filesystem initialization.
+// Setup is a command which can do global or per-filesystem initialization.
var Setup = cli.Command{
Name: "setup",
ArgsUsage: fmt.Sprintf("[%s]", mountpointArg),
Usage: "perform global setup or filesystem setup",
Description: fmt.Sprintf(`This command creates fscrypt's global config
- file or enables fscrypt on a filesystem.
-
- (1) When used without %[1]s, create the parameters in %[2]s.
- This is primarily used to configure the passphrase hashing
- parameters to the appropriate hardness (as determined by %[3]s).
- Being root is required to write the config file.
-
- (2) When used with %[1]s, enable fscrypt on %[1]s. This involves
- creating the necessary folders on the filesystem which will hold
- the metadata structures. Begin root may be required to create
- these folders.`, mountpointArg, actions.ConfigFileLocation,
+ file and/or prepares a filesystem for use with fscrypt.
+
+ (1) When used without %[1]s, this command creates the global
+ config file %[2]s and the fscrypt metadata directory for the
+ root filesystem (i.e. /.fscrypt). This requires root privileges.
+ The passphrase hashing parameters in %[2]s are automatically set
+ to an appropriate hardness, as determined by %[3]s. The root
+ filesystem's metadata directory is created even if the root
+ filesystem doesn't support encryption itself, since it's where
+ login passphrase protectors are stored.
+
+ (2) When used with %[1]s, this command creates the fscrypt
+ metadata directory for the filesystem mounted at %[1]s. This
+ allows fscrypt to be used on that filesystem, provided that any
+ kernel and filesystem-specific prerequisites are also met (see
+ the README). This may require root privileges.`,
+ mountpointArg, actions.ConfigFileLocation,
shortDisplay(timeTargetFlag)),
- Flags: []cli.Flag{timeTargetFlag, legacyFlag, forceFlag},
+ Flags: []cli.Flag{timeTargetFlag, forceFlag, allUsersSetupFlag},
Action: setupAction,
}
func setupAction(c *cli.Context) error {
- var err error
switch c.NArg() {
case 0:
// Case (1) - global setup
- err = createGlobalConfig(c.App.Writer, actions.ConfigFileLocation)
+ if err := createGlobalConfig(c.App.Writer, actions.ConfigFileLocation); err != nil {
+ return newExitError(c, err)
+ }
+ if err := setupFilesystem(c.App.Writer, actions.LoginProtectorMountpoint); err != nil {
+ if _, ok := err.(*filesystem.ErrAlreadySetup); !ok {
+ return newExitError(c, err)
+ }
+ fmt.Fprintf(c.App.Writer,
+ "Skipping creating %s because it already exists.\n",
+ filepath.Join(actions.LoginProtectorMountpoint, ".fscrypt"))
+ }
case 1:
// Case (2) - filesystem setup
- err = setupFilesystem(c.App.Writer, c.Args().Get(0))
+ if err := setupFilesystem(c.App.Writer, c.Args().Get(0)); err != nil {
+ return newExitError(c, err)
+ }
default:
return expectedArgsErr(c, 1, true)
}
-
- if err != nil {
- return newExitError(c, err)
- }
return nil
}
@@ -90,7 +107,7 @@ var Encrypt = cli.Command{
immediately be used.`, directoryArg, shortDisplay(policyFlag),
shortDisplay(protectorFlag), mountpointArg),
Flags: []cli.Flag{policyFlag, unlockWithFlag, protectorFlag, sourceFlag,
- userFlag, nameFlag, keyFileFlag, skipUnlockFlag},
+ userFlag, nameFlag, keyFileFlag, skipUnlockFlag, noRecoveryFlag},
Action: encryptAction,
}
@@ -104,6 +121,13 @@ func encryptAction(c *cli.Context) error {
return newExitError(c, err)
}
+ // Most people expect that other users can't see their encrypted files
+ // while they're unlocked, so change the directory's mode to 0700.
+ if err := os.Chmod(path, 0700); err != nil {
+ fmt.Fprintf(c.App.Writer, "Warning: unable to chmod %q to 0700 [%v]\n", path, err)
+ // Continue on; don't consider this a fatal error.
+ }
+
if !skipUnlockFlag.Value {
fmt.Fprintf(c.App.Writer,
"%q is now encrypted, unlocked, and ready for use.\n", path)
@@ -115,15 +139,71 @@ 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
+}
+
+func writeRecoveryInstructions(recoveryPassphrase *crypto.Key, recoveryProtector *actions.Protector,
+ policy *actions.Policy, dirPath string) error {
+ if recoveryPassphrase == nil {
+ return nil
+ }
+ recoveryFile := filepath.Join(dirPath, "fscrypt_recovery_readme.txt")
+ if err := actions.WriteRecoveryInstructions(recoveryPassphrase, recoveryProtector,
+ policy, recoveryFile); err != nil {
+ return err
+ }
+ msg := fmt.Sprintf(`See %q for important recovery instructions.
+ It is *strongly recommended* to record the recovery passphrase in a
+ secure location; otherwise you will lose access to this directory if you
+ reinstall the operating system or move this filesystem to another
+ system.`, recoveryFile)
+ hdr := "IMPORTANT: "
+ fmt.Print("\n" + hdr + wrapText(msg, len(hdr)) + "\n\n")
+ 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) {
- target, err := parseUserFlag(!skipUnlockFlag.Value)
+ targetUser, err := parseUserFlag()
if err != nil {
return
}
- ctx, err := actions.NewContextFromPath(path, target)
+ ctx, err := actions.NewContextFromPath(path, targetUser)
if err != nil {
return
}
@@ -132,20 +212,37 @@ func encryptPath(path string) (err error) {
}
var policy *actions.Policy
+ var recoveryPassphrase *crypto.Key
+ var recoveryProtector *actions.Protector
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
+ }
+ defer policy.Lock()
+
+ 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 {
return protErr
}
defer func() {
protector.Lock()
+ // Successfully created protector should be reverted on failure.
if err != nil && created {
protector.Revert()
}
@@ -154,39 +251,70 @@ func encryptPath(path string) (err error) {
if err = protector.Unlock(existingKeyFn); err != nil {
return
}
- policy, err = actions.CreatePolicy(ctx, protector)
- }
- // Successfully created policy should be reverted on failure.
- if err != nil {
- return
- }
- defer func() {
- policy.Lock()
- if err != nil {
- policy.Deprovision()
- policy.Revert()
+ if policy, err = actions.CreatePolicy(ctx, protector); err != nil {
+ return
}
- }()
+ defer func() {
+ policy.Lock()
+ // Successfully created policy should be reverted on failure.
+ if err != nil {
+ policy.Revert()
+ }
+ }()
- // Unlock() first, so if the Unlock() fails the directory isn't changed.
- if !skipUnlockFlag.Value {
+ // Generate a recovery passphrase if needed.
+ if ctx.Mount != protector.Context.Mount && !noRecoveryFlag.Value {
+ if recoveryPassphrase, recoveryProtector, err = actions.AddRecoveryPassphrase(
+ policy, filepath.Base(path)); err != nil {
+ return
+ }
+ defer func() {
+ recoveryPassphrase.Wipe()
+ recoveryProtector.Lock()
+ // Successfully created protector should be reverted on failure.
+ if err != nil {
+ recoveryProtector.Revert()
+ }
+ }()
+ }
+ }
+
+ // 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
}
+ defer func() {
+ if err != nil || skipUnlockFlag.Value {
+ policy.Deprovision(false)
+ }
+ }()
}
- if err = policy.Apply(path); os.IsPermission(errors.Cause(err)) {
- // EACCES at this point indicates ownership issues.
- err = errors.Wrap(ErrBadOwners, path)
+ if err = policy.Apply(path); err != nil {
+ return
}
- return
+ return writeRecoveryInstructions(recoveryPassphrase, recoveryProtector, policy, path)
}
// checkEncryptable returns an error if the path cannot be encrypted.
func checkEncryptable(ctx *actions.Context, path string) error {
- log.Printf("ensuring %s is an empty and readable directory", path)
+
+ log.Printf("checking whether %q is already encrypted", path)
+ if _, err := metadata.GetPolicy(path); err == nil {
+ return &metadata.ErrAlreadyEncrypted{Path: path}
+ }
+
+ log.Printf("checking whether filesystem %s supports encryption", ctx.Mount.Path)
+ if err := ctx.Mount.CheckSupport(); err != nil {
+ return err
+ }
+
+ log.Printf("checking whether %q is an empty and readable directory", path)
f, err := os.Open(path)
if err != nil {
return err
@@ -196,29 +324,17 @@ func checkEncryptable(ctx *actions.Context, path string) error {
switch names, err := f.Readdirnames(-1); {
case err != nil:
// Could not read directory (might not be a directory)
- log.Print(errors.Wrap(err, path))
- return errors.Wrap(ErrNotEmptyDir, path)
- case len(names) > 0:
- log.Printf("directory %s is not empty", path)
- return errors.Wrap(ErrNotEmptyDir, path)
- }
-
- log.Printf("ensuring %s supports encryption and filesystem is using fscrypt", path)
- switch _, err := actions.GetPolicyFromPath(ctx, path); errors.Cause(err) {
- case metadata.ErrNotEncrypted:
- // We are not encrypted. Finally, we check that the filesystem
- // supports encryption
- return ctx.Mount.CheckSupport()
- case nil:
- // We are encrypted
- return errors.Wrap(metadata.ErrEncrypted, path)
- default:
+ err = errors.Wrap(err, path)
+ log.Print(err)
return err
+ case len(names) > 0:
+ return &ErrDirNotEmpty{path}
}
+ return err
}
// selectOrCreateProtector uses user input (or flags) to either create a new
-// protector or select and existing one. The boolean return value is true if we
+// protector or select an existing one. The boolean return value is true if we
// created a new protector.
func selectOrCreateProtector(ctx *actions.Context) (*actions.Protector, bool, error) {
if protectorFlag.Value != "" {
@@ -262,8 +378,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,
@@ -274,12 +390,12 @@ func unlockAction(c *cli.Context) error {
return expectedArgsErr(c, 1, false)
}
- target, err := parseUserFlag(true)
+ targetUser, err := parseUserFlag()
if err != nil {
return newExitError(c, err)
}
path := c.Args().Get(0)
- ctx, err := actions.NewContextFromPath(path, target)
+ ctx, err := actions.NewContextFromPath(path, targetUser)
if err != nil {
return newExitError(c, err)
}
@@ -290,10 +406,15 @@ 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())
- return newExitError(c, errors.Wrapf(ErrPolicyUnlocked, path))
+ if policy.IsProvisionedByTargetUser() {
+ log.Printf("policy %s is already provisioned by %v",
+ policy.Descriptor(), ctx.TargetUser.Username)
+ return newExitError(c, errors.Wrap(ErrDirAlreadyUnlocked, path))
}
if err := policy.Unlock(optionFn, existingKeyFn); err != nil {
@@ -309,6 +430,163 @@ 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, allUsersLockFlag},
+ 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 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(allUsersLockFlag.Value); err != nil {
+ switch err {
+ case keyring.ErrKeyNotPresent:
+ break
+ case keyring.ErrKeyAddedByOtherUsers:
+ return newExitError(c, &ErrDirUnlockedByOtherUsers{path})
+ case keyring.ErrKeyFilesOpen:
+ return newExitError(c, &ErrDirFilesOpen{path})
+ default:
+ 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.Wrap(ErrDirAlreadyLocked, path))
+ }
+ }
+
+ if policy.NeedsUserKeyring() {
+ if err = dropCachesIfRequested(c, ctx); err != nil {
+ return newExitError(c, err)
+ }
+ if isDirUnlockedHeuristic(path) {
+ return newExitError(c, &ErrDirFilesOpen{path})
+ }
+ }
+
+ fmt.Fprintf(c.App.Writer, "%q is now locked.\n", path)
+ return nil
+}
+
+func isPossibleNoKeyName(filename string) bool {
+ // No-key names are at least 22 bytes long, since they are
+ // base64-encoded and ciphertext filenames are at least 16 bytes.
+ if len(filename) < 22 {
+ return false
+ }
+ // On the latest kernels, no-key names contain only base64url characters
+ // (A-Z, a-z, 0-9, -, and _). On older kernels, the + and , characters
+ // were used too. Allow all of these characters.
+ validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_+,"
+ for _, char := range filename {
+ if !strings.ContainsRune(validChars, char) {
+ return false
+ }
+ }
+ return true
+}
+
+// isDirUnlockedHeuristic returns true if the directory is definitely still
+// unlocked. This is the case if we can create a subdirectory or if the
+// directory contains filenames that aren't valid no-key names. 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
+ }
+ dir, err := os.Open(dirPath)
+ if err != nil {
+ return false
+ }
+ defer dir.Close()
+
+ names, err := dir.Readdirnames(-1)
+ if err != nil {
+ return false
+ }
+ for _, name := range names {
+ if !isPossibleNoKeyName(name) {
+ return true
+ }
+ }
+ return false
+}
+
// Purge removes all the policy keys from the keyring (also need unmount).
var Purge = cli.Command{
Name: "purge",
@@ -358,15 +636,18 @@ func purgeAction(c *cli.Context) error {
}
}
- target, err := parseUserFlag(true)
+ targetUser, err := parseUserFlag()
if err != nil {
return newExitError(c, err)
}
mountpoint := c.Args().Get(0)
- ctx, err := actions.NewContextFromMountpoint(mountpoint, target)
+ ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser)
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 {
@@ -382,13 +663,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 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
}
@@ -429,17 +705,15 @@ func statusAction(c *cli.Context) error {
err = writeGlobalStatus(c.App.Writer)
case 1:
path := c.Args().Get(0)
- ctx, mntErr := actions.NewContextFromMountpoint(path, nil)
- switch errors.Cause(mntErr) {
- case nil:
+ var ctx *actions.Context
+ ctx, err = actions.NewContextFromMountpoint(path, nil)
+ if err == nil {
// Case (2) - mountpoint status
err = writeFilesystemStatus(c.App.Writer, ctx)
- case filesystem.ErrNotAMountpoint:
+ } else if _, ok := err.(*filesystem.ErrNotAMountpoint); ok {
// Case (3) - file or directory status
err = writePathStatus(c.App.Writer, path)
- default:
- err = mntErr
}
default:
return expectedArgsErr(c, 1, true)
@@ -474,7 +748,7 @@ var Metadata = cli.Command{
(4) Changing the protector protecting a policy using the
"add-protector-to-policy" and "remove-protector-from-policy"
subcommands.`,
- Subcommands: []cli.Command{createMetadata, destoryMetadata, changePassphrase,
+ Subcommands: []cli.Command{createMetadata, destroyMetadata, changePassphrase,
addProtectorToPolicy, removeProtectorFromPolicy, dumpMetadata},
}
@@ -508,18 +782,18 @@ func createProtectorAction(c *cli.Context) error {
return expectedArgsErr(c, 1, false)
}
- target, err := parseUserFlag(false)
+ targetUser, err := parseUserFlag()
if err != nil {
return newExitError(c, err)
}
mountpoint := c.Args().Get(0)
- ctx, err := actions.NewContextFromMountpoint(mountpoint, target)
+ ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser)
if err != nil {
return newExitError(c, err)
}
prompt := fmt.Sprintf("Create new protector on %q", ctx.Mount.Path)
- if err := askConfirmation(prompt, true, ""); err != nil {
+ if err = askConfirmation(prompt, true, ""); err != nil {
return newExitError(c, err)
}
@@ -537,8 +811,8 @@ func createProtectorAction(c *cli.Context) error {
var createPolicy = cli.Command{
Name: "policy",
ArgsUsage: fmt.Sprintf("%s %s", mountpointArg, shortDisplay(protectorFlag)),
- Usage: "create a new protector on a filesystem",
- Description: fmt.Sprintf(`This command creates a new protector on %s
+ Usage: "create a new policy on a filesystem",
+ Description: fmt.Sprintf(`This command creates a new policy on %s
that has not (yet) been applied to any directory. After
creation, the user can use %s with "fscrypt encrypt" to encrypt
a directory with this new policy. As all policies must be
@@ -561,20 +835,20 @@ func createPolicyAction(c *cli.Context) error {
return newExitError(c, err)
}
- if err := checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil {
+ if err = checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil {
return err
}
protector, err := getProtectorFromFlag(protectorFlag.Value, ctx.TargetUser)
if err != nil {
return newExitError(c, err)
}
- if err := protector.Unlock(existingKeyFn); err != nil {
+ if err = protector.Unlock(existingKeyFn); err != nil {
return newExitError(c, err)
}
defer protector.Lock()
prompt := fmt.Sprintf("Create new policy on %q", ctx.Mount.Path)
- if err := askConfirmation(prompt, true, ""); err != nil {
+ if err = askConfirmation(prompt, true, ""); err != nil {
return newExitError(c, err)
}
@@ -589,7 +863,7 @@ func createPolicyAction(c *cli.Context) error {
return nil
}
-var destoryMetadata = cli.Command{
+var destroyMetadata = cli.Command{
Name: "destroy",
ArgsUsage: fmt.Sprintf("[%s | %s | %s]", shortDisplay(protectorFlag),
shortDisplay(policyFlag), mountpointArg),
@@ -616,10 +890,10 @@ var destoryMetadata = cli.Command{
shortDisplay(protectorFlag), shortDisplay(policyFlag),
mountpointArg),
Flags: []cli.Flag{protectorFlag, policyFlag, forceFlag},
- Action: destoryMetadataAction,
+ Action: destroyMetadataAction,
}
-func destoryMetadataAction(c *cli.Context) error {
+func destroyMetadataAction(c *cli.Context) error {
switch c.NArg() {
case 0:
switch {
@@ -759,6 +1033,9 @@ func addProtectorAction(c *cli.Context) error {
}
// Sanity check before unlocking everything
if err := policy.AddProtector(protector); errors.Cause(err) != actions.ErrLocked {
+ if err == nil {
+ err = errors.New("policy and protector are not locked")
+ }
return newExitError(c, err)
}
@@ -806,29 +1083,30 @@ func removeProtectorAction(c *cli.Context) error {
return err
}
- // We do not need to unlock anything for this operation
- protector, err := getProtectorFromFlag(protectorFlag.Value, nil)
+ // We only need the protector descriptor, not the protector itself.
+ ctx, protectorDescriptor, err := parseMetadataFlag(protectorFlag.Value, nil)
if err != nil {
return newExitError(c, err)
}
- policy, err := getPolicyFromFlag(policyFlag.Value, protector.Context.TargetUser)
+ // We don't need to unlock the policy for this operation.
+ policy, err := getPolicyFromFlag(policyFlag.Value, ctx.TargetUser)
if err != nil {
return newExitError(c, err)
}
prompt := fmt.Sprintf("Stop protecting policy %s with protector %s?",
- policy.Descriptor(), protector.Descriptor())
+ policy.Descriptor(), protectorDescriptor)
warning := "All files using this policy will NO LONGER be accessible with this protector!!"
if err := askConfirmation(prompt, false, warning); err != nil {
return newExitError(c, err)
}
- if err := policy.RemoveProtector(protector); err != nil {
+ if err := policy.RemoveProtector(protectorDescriptor); err != nil {
return newExitError(c, err)
}
fmt.Fprintf(c.App.Writer, "Protector %s no longer protecting policy %s.\n",
- protector.Descriptor(), policy.Descriptor())
+ protectorDescriptor, policy.Descriptor())
return nil
}
diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go
index 81a6798..e4da884 100644
--- a/cmd/fscrypt/errors.go
+++ b/cmd/fscrypt/errors.go
@@ -30,12 +30,13 @@ import (
"github.com/pkg/errors"
"github.com/urfave/cli"
+ "golang.org/x/sys/unix"
"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"
)
@@ -45,8 +46,7 @@ const failureExitCode = 1
// Various errors used for the top level user interface
var (
ErrCanceled = errors.New("operation canceled")
- ErrNoDesctructiveOps = errors.New("operation would be destructive")
- ErrMaxPassphrase = util.SystemError("max passphrase length exceeded")
+ ErrNoDestructiveOps = errors.New("operation would be destructive")
ErrInvalidSource = errors.New("invalid source type")
ErrPassphraseMismatch = errors.New("entered passphrases do not match")
ErrSpecifyProtector = errors.New("multiple protectors available")
@@ -55,15 +55,47 @@ var (
ErrKeyFileLength = errors.Errorf("key file must be %d bytes", metadata.InternalKeyLen)
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")
- ErrBadOwners = errors.New("you do not own this directory")
- ErrNotEmptyDir = errors.New("not an empty directory")
+ ErrDirAlreadyUnlocked = errors.New("this file or directory is already unlocked")
+ ErrDirAlreadyLocked = errors.New("this file or directory is already locked")
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")
)
+// ErrDirFilesOpen indicates that a directory can't be fully locked because
+// files protected by the directory's policy are still open.
+type ErrDirFilesOpen struct {
+ DirPath string
+}
+
+func (err *ErrDirFilesOpen) Error() string {
+ return `Directory was incompletely locked because some files are still
+ open. These files remain accessible.`
+}
+
+// ErrDirUnlockedByOtherUsers indicates that a directory can't be locked because
+// the directory's policy is still provisioned by other users.
+type ErrDirUnlockedByOtherUsers struct {
+ DirPath string
+}
+
+func (err *ErrDirUnlockedByOtherUsers) Error() string {
+ return fmt.Sprintf(`Directory %q couldn't be fully locked because other
+ user(s) have unlocked it.`, err.DirPath)
+}
+
+// ErrDirNotEmpty indicates that a directory can't be encrypted because it's not
+// empty.
+type ErrDirNotEmpty struct {
+ DirPath string
+}
+
+func (err *ErrDirNotEmpty) Error() string {
+ return fmt.Sprintf("Directory %q cannot be encrypted because it is non-empty.", err.DirPath)
+}
+
var loadHelpText = fmt.Sprintf("You may need to mount a linked filesystem. Run with %s for more information.", shortDisplay(verboseFlag))
// getFullName returns the full name of the application or command being used.
@@ -74,67 +106,188 @@ func getFullName(c *cli.Context) string {
return c.App.HelpName
}
+func isGrubInstalledOnFilesystem(mnt *filesystem.Mount) bool {
+ dir := filepath.Join(mnt.Path, "boot/grub")
+ grubDirMount, _ := filesystem.FindMount(dir)
+ return grubDirMount == mnt
+}
+
+func suggestEnablingEncryption(mnt *filesystem.Mount) string {
+ kconfig := "CONFIG_FS_ENCRYPTION=y"
+ switch mnt.FilesystemType {
+ case "ext4":
+ // Recommend running tune2fs -O encrypt. But be really careful;
+ // old kernels didn't support block_size != PAGE_SIZE, and old
+ // GRUB didn't support encryption.
+ var statfs unix.Statfs_t
+ if err := unix.Statfs(mnt.Path, &statfs); err != nil {
+ return ""
+ }
+ pagesize := os.Getpagesize()
+ if int64(statfs.Bsize) != int64(pagesize) && !util.IsKernelVersionAtLeast(5, 5) {
+ return fmt.Sprintf(`This filesystem uses a block size
+ (%d) other than the system page size (%d). Ext4
+ encryption didn't support this case until kernel v5.5.
+ Do *not* enable encryption on this filesystem. Either
+ upgrade your kernel to v5.5 or later, or re-create this
+ filesystem using 'mkfs.ext4 -b %d -O encrypt %s'
+ (WARNING: that will erase all data on it).`,
+ statfs.Bsize, pagesize, pagesize, mnt.Device)
+ }
+ if !util.IsKernelVersionAtLeast(5, 1) {
+ kconfig = "CONFIG_EXT4_ENCRYPTION=y"
+ }
+ s := fmt.Sprintf(`To enable encryption support on this
+ filesystem, run:
+
+ > sudo tune2fs -O encrypt %q
+ `, mnt.Device)
+ if isGrubInstalledOnFilesystem(mnt) {
+ s += `
+ WARNING: you seem to have GRUB installed on this
+ filesystem. Before doing the above, make sure you are
+ using GRUB v2.04 or later; otherwise your system will
+ become unbootable.
+ `
+ }
+ s += fmt.Sprintf(`
+ Also ensure that your kernel has %s. See the documentation for
+ more details.`, kconfig)
+ return s
+ case "f2fs":
+ if !util.IsKernelVersionAtLeast(5, 1) {
+ kconfig = "CONFIG_F2FS_FS_ENCRYPTION=y"
+ }
+ return fmt.Sprintf(`To enable encryption support on this
+ filesystem, you'll need to run:
+
+ > sudo fsck.f2fs -O encrypt %q
+
+ Also ensure that your kernel has %s. See the documentation for
+ more details.`, mnt.Device, kconfig)
+ default:
+ return `See the documentation for how to enable encryption
+ support on this filesystem.`
+ }
+}
+
// getErrorSuggestions returns a string containing suggestions about how to fix
// an error. If no suggestion is necessary or available, return empty string.
func getErrorSuggestions(err error) string {
+ switch e := err.(type) {
+ case *ErrDirFilesOpen:
+ return fmt.Sprintf(`Try killing any processes using files in the
+ directory, for example using:
+
+ > find %q -print0 | xargs -0 fuser -k
+
+ Then re-run:
+
+ > fscrypt lock %q`, e.DirPath, e.DirPath)
+ case *ErrDirNotEmpty:
+ dir := filepath.Clean(e.DirPath)
+ newDir := dir + ".new"
+ return fmt.Sprintf(`Files cannot be encrypted in-place. Instead,
+ encrypt a new directory, copy the files into it, and securely
+ delete the original directory. For example:
+
+ > mkdir %q
+ > fscrypt encrypt %q
+ > cp -a -T %q %q
+ > find %q -type f -print0 | xargs -0 shred -n1 --remove=unlink
+ > rm -rf %q
+ > mv %q %q
+
+ Caution: due to the nature of modern storage devices and filesystems,
+ the original data may still be recoverable from disk. It's much better
+ to encrypt your files from the start.`, newDir, newDir, dir, newDir, dir, dir, newDir, dir)
+ case *ErrDirUnlockedByOtherUsers:
+ return fmt.Sprintf(`If you want to force the directory to be
+ locked, use:
+
+ > sudo fscrypt lock --all-users %q`, e.DirPath)
+ case *actions.ErrBadConfigFile:
+ return `Either fix this file manually, or run "sudo fscrypt setup" to recreate it.`
+ case *actions.ErrLoginProtectorName:
+ return fmt.Sprintf("To fix this, don't specify the %s option.", shortDisplay(nameFlag))
+ case *actions.ErrMissingProtectorName:
+ return fmt.Sprintf("Use %s to specify a protector name.", shortDisplay(nameFlag))
+ case *actions.ErrNoConfigFile:
+ return `Run "sudo fscrypt setup" to create this file.`
+ case *filesystem.ErrEncryptionNotEnabled:
+ return suggestEnablingEncryption(e.Mount)
+ case *filesystem.ErrEncryptionNotSupported:
+ switch e.Mount.FilesystemType {
+ case "ext4":
+ if !util.IsKernelVersionAtLeast(4, 1) {
+ return "ext4 encryption requires kernel v4.1 or later."
+ }
+ case "f2fs":
+ if !util.IsKernelVersionAtLeast(4, 2) {
+ return "f2fs encryption requires kernel v4.2 or later."
+ }
+ case "ubifs":
+ if !util.IsKernelVersionAtLeast(4, 10) {
+ return "ubifs encryption requires kernel v4.10 or later."
+ }
+ case "ceph":
+ if !util.IsKernelVersionAtLeast(6, 6) {
+ return "CephFS encryption requires kernel v6.6 or later."
+ }
+ }
+ return ""
+ case *filesystem.ErrNoCreatePermission:
+ return `For how to allow users to create fscrypt metadata on a
+ filesystem, refer to
+ https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem`
+ case *filesystem.ErrNotSetup:
+ return fmt.Sprintf(`Run "sudo fscrypt setup %s" to use fscrypt
+ on this filesystem.`, e.Mount.Path)
+ 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))
+ case *keyring.ErrSessionUserKeyring:
+ 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 *metadata.ErrLockedRegularFile:
+ return `It is not possible to operate directly on a locked
+ regular file, since the kernel does not support this.
+ Specify the parent directory instead. (For loose files,
+ any directory with the file's policy works.)`
+ }
switch errors.Cause(err) {
- case filesystem.ErrNotSetup:
- return fmt.Sprintf(`Run "fscrypt setup %s" to use fscrypt on this filesystem.`, mountpointArg)
- case crypto.ErrKeyLock:
+ case crypto.ErrMlockUlimit:
return `Too much memory was requested to be locked in RAM. The
current limit for this user can be checked with "ulimit
-l". The limit can be modified by either changing the
"memlock" item in /etc/security/limits.conf or by
changing the "LimitMEMLOCK" value in systemd.`
- case metadata.ErrEncryptionNotSupported:
- return `Encryption for this type of filesystem is not supported
- on this kernel version.`
- case metadata.ErrEncryptionNotEnabled:
- return `Encryption is either disabled in the kernel config, or
- 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:
- 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:
- return fmt.Sprintf(`You can only use %s to access the user
- keyring of another user if you are running as root.`,
- shortDisplay(userFlag))
- case actions.ErrBadConfigFile:
- return `Run "sudo fscrypt setup" to recreate the file.`
- case actions.ErrNoConfigFile:
- return `Run "sudo fscrypt setup" to create the file.`
- case actions.ErrMissingPolicyMetadata:
- return `This file or directory has either been encrypted with
- another tool (such as e4crypt) or the corresponding
- filesystem metadata has been deleted.`
- case actions.ErrPolicyMetadataMismatch:
- return `The metadata for this encrypted directory is in an
- inconsistent state. This most likely means the filesystem
- metadata is corrupted.`
- case actions.ErrMissingProtectorName:
- return fmt.Sprintf("Use %s to specify a protector name.", shortDisplay(nameFlag))
- case ErrNoDesctructiveOps:
- return fmt.Sprintf("Use %s to automatically run destructive operations.", shortDisplay(forceFlag))
+ case keyring.ErrV2PoliciesUnsupported:
+ return fmt.Sprintf(`v2 encryption policies are only supported by kernel
+ version 5.4 and later. Either use a newer kernel, or change
+ policy_version to 1 in %s.`, actions.ConfigFileLocation)
+ case ErrNoDestructiveOps:
+ return fmt.Sprintf("If desired, use %s to automatically run destructive operations.",
+ shortDisplay(forceFlag))
case ErrSpecifyProtector:
- return fmt.Sprintf("Use %s to specify a protector.", shortDisplay(protectorFlag))
+ return fmt.Sprintf("Use %s or %s to specify a protector.",
+ shortDisplay(protectorFlag), shortDisplay(unlockWithFlag))
case ErrSpecifyKeyFile:
return fmt.Sprintf("Use %s to specify a key file.", shortDisplay(keyFileFlag))
- case ErrBadOwners:
- return `Encryption can only be setup on directories you own,
- even if you have write permission for the directory.`
- case ErrNotEmptyDir:
- return `Encryption can only be setup on empty directories; files
- cannot be encrypted in-place. Instead, encrypt an empty
- directory, copy the files into that encrypted directory,
- and securely delete the originals with "shred".`
case ErrDropCachesPerm:
return fmt.Sprintf(`Either this command should be run as root to
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
@@ -151,12 +304,12 @@ func getErrorSuggestions(err error) string {
}
// newExitError creates a new error for a given context and normal error. The
-// returned error prepends the name of the relevant command and will make
-// fscrypt return a non-zero exit value.
+// returned error prepends an error tag and the name of the relevant command,
+// and it will make fscrypt return a non-zero exit value.
func newExitError(c *cli.Context, err error) error {
- // Prepend the full name and append suggestions (if any)
- fullNamePrefix := getFullName(c) + ": "
- message := fullNamePrefix + wrapText(err.Error(), utf8.RuneCountInString(fullNamePrefix))
+ // Prepend the error tag and full name, and append suggestions (if any)
+ prefix := "[ERROR] " + getFullName(c) + ": "
+ message := prefix + wrapText(err.Error(), utf8.RuneCountInString(prefix))
if suggestion := getErrorSuggestions(err); suggestion != "" {
message += "\n\n" + wrapText(suggestion, 0)
@@ -165,8 +318,8 @@ func newExitError(c *cli.Context, err error) error {
return cli.NewExitError(message, failureExitCode)
}
-// usageError implements cli.ExitCoder to will print the usage and the return a
-// non-zero value. This error should be used when a command is used incorrectly.
+// usageError implements cli.ExitCoder to print the usage and return a non-zero
+// value. This error should be used when a command is used incorrectly.
type usageError struct {
c *cli.Context
message string
diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go
index 5137eff..3d3c51d 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"
)
@@ -115,9 +114,10 @@ var (
// UPDATE THIS ARRAY WHEN ADDING NEW FLAGS!!!
// TODO(joerichey) add presubmit rule to enforce this
allFlags = []prettyFlag{helpFlag, versionFlag, verboseFlag, quietFlag,
- forceFlag, legacyFlag, skipUnlockFlag, timeTargetFlag,
+ forceFlag, skipUnlockFlag, timeTargetFlag,
sourceFlag, nameFlag, keyFileFlag, protectorFlag,
- unlockWithFlag, policyFlag}
+ unlockWithFlag, policyFlag, allUsersLockFlag, allUsersSetupFlag,
+ noRecoveryFlag}
// universalFlags contains flags that should be on every command
universalFlags = []cli.Flag{verboseFlag, quietFlag, helpFlag}
)
@@ -130,7 +130,7 @@ var (
}
versionFlag = &boolFlag{
Name: "version",
- Usage: `Prints version and license information.`,
+ Usage: `Prints version information.`,
}
verboseFlag = &boolFlag{
Name: "verbose",
@@ -144,16 +144,10 @@ var (
}
forceFlag = &boolFlag{
Name: "force",
- Usage: fmt.Sprintf(`Suppresses all confirmation prompts and
- warnings, causing any action to automatically proceed.
- WARNING: This bypasses confirmations for protective
- operations, use with care.`),
- }
- legacyFlag = &boolFlag{
- Name: "legacy",
- Usage: `Allow for support of older kernels with ext4 (before
- v4.8) and F2FS (before v4.6) filesystems.`,
- Default: true,
+ Usage: `Suppresses all confirmation prompts and warnings,
+ causing any action to automatically proceed. WARNING:
+ This bypasses confirmations for protective operations,
+ use with care.`,
}
skipUnlockFlag = &boolFlag{
Name: "skip-unlock",
@@ -163,12 +157,35 @@ 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,
}
+ allUsersLockFlag = &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.`,
+ }
+ allUsersSetupFlag = &boolFlag{
+ Name: "all-users",
+ Usage: `When setting up a filesystem for fscrypt, allow users
+ other than the calling user (typically root) to create
+ fscrypt policies and protectors on the filesystem. Note
+ that this will create a world-writable directory, which
+ users could use to fill up the entire filesystem. Hence,
+ this option may not be appropriate for some systems.`,
+ }
+ noRecoveryFlag = &boolFlag{
+ Name: "no-recovery",
+ Usage: `Don't generate a recovery passphrase.`,
+ }
)
// Option flags: used to specify options instead of being prompted for them
@@ -205,12 +222,13 @@ var (
Usage: `Use the contents of FILE as the wrapping key when
creating or unlocking raw_key protectors. FILE should be
formatted as raw binary and should be exactly 32 bytes
- long.`,
+ long. When running non-interactively and no key is provided,
+ will try to read the key from stdin.`,
}
userFlag = &stringFlag{
Name: "user",
ArgName: "USERNAME",
- Usage: `Specifiy which user should be used for login passphrases
+ Usage: `Specify which user should be used for login passphrases
or to which user's keyring keys should be provisioned.`,
}
protectorFlag = &stringFlag{
@@ -255,18 +273,18 @@ func matchMetadataFlag(flagValue string) (mountpoint, descriptor string, err err
// parseMetadataFlag takes the value of either protectorFlag or policyFlag
// formatted as MOUNTPOINT:DESCRIPTOR, and returns a context for the mountpoint
// and a string for the descriptor.
-func parseMetadataFlag(flagValue string, target *user.User) (*actions.Context, string, error) {
+func parseMetadataFlag(flagValue string, targetUser *user.User) (*actions.Context, string, error) {
mountpoint, descriptor, err := matchMetadataFlag(flagValue)
if err != nil {
return nil, "", err
}
- ctx, err := actions.NewContextFromMountpoint(mountpoint, target)
+ ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser)
return ctx, descriptor, err
}
// getProtectorFromFlag gets an existing locked protector from protectorFlag.
-func getProtectorFromFlag(flagValue string, target *user.User) (*actions.Protector, error) {
- ctx, descriptor, err := parseMetadataFlag(flagValue, target)
+func getProtectorFromFlag(flagValue string, targetUser *user.User) (*actions.Protector, error) {
+ ctx, descriptor, err := parseMetadataFlag(flagValue, targetUser)
if err != nil {
return nil, err
}
@@ -274,8 +292,8 @@ func getProtectorFromFlag(flagValue string, target *user.User) (*actions.Protect
}
// getPolicyFromFlag gets an existing locked policy from policyFlag.
-func getPolicyFromFlag(flagValue string, target *user.User) (*actions.Policy, error) {
- ctx, descriptor, err := parseMetadataFlag(flagValue, target)
+func getPolicyFromFlag(flagValue string, targetUser *user.User) (*actions.Policy, error) {
+ ctx, descriptor, err := parseMetadataFlag(flagValue, targetUser)
if err != nil {
return nil, err
}
@@ -283,24 +301,10 @@ func getPolicyFromFlag(flagValue string, target *user.User) (*actions.Policy, er
}
// 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/format.go b/cmd/fscrypt/format.go
index ef009d3..21253ad 100644
--- a/cmd/fscrypt/format.go
+++ b/cmd/fscrypt/format.go
@@ -25,12 +25,11 @@ import (
"bytes"
"fmt"
"os"
- "regexp"
"strings"
"unicode/utf8"
"github.com/urfave/cli"
- "golang.org/x/crypto/ssh/terminal"
+ "golang.org/x/term"
"github.com/google/fscrypt/util"
)
@@ -65,8 +64,8 @@ func init() {
flagPaddingLength = maxShortDisplay + 2*indentLength
// We use the width of the terminal unless we cannot get the width.
- width, _, err := terminal.GetSize(int(os.Stdout.Fd()))
- if err != nil {
+ width, _, err := term.GetSize(int(os.Stdout.Fd()))
+ if err != nil || width <= 0 {
lineLength = fallbackLineLength
} else {
lineLength = util.MinInt(width, maxLineLength)
@@ -74,7 +73,7 @@ func init() {
}
-// Flags that conform to this interface can be used with an urfave/cli
+// Flags that conform to this interface can be used with a urfave/cli
// application and can be printed in the correct format.
type prettyFlag interface {
cli.Flag
@@ -83,8 +82,10 @@ type prettyFlag interface {
}
// How a flag should appear on the command line. We have two formats:
-// --name
-// --name=ARG_NAME
+//
+// --name
+// --name=ARG_NAME
+//
// The ARG_NAME appears if the prettyFlag's GetArgName() method returns a
// non-empty string. The returned string from shortDisplay() does not include
// any leading or trailing whitespace.
@@ -97,22 +98,20 @@ func shortDisplay(f prettyFlag) string {
// How our flags should appear when displaying their usage. An example would be:
//
-// --help Prints help screen for commands and subcommands.
-//
-// If a default is specified, this if appended to the usage. Example:
+// --help Prints help screen for commands and subcommands.
//
-// --legacy Allow for support of older kernels with ext4
-// (before v4.8) and F2FS (before v4.6) filesystems.
-// (default: true)
+// If a default is specified, then it is appended to the usage. Example:
//
+// --time=TIME Calibrate passphrase hashing to take the
+// specified amount of TIME (default: 1s)
func longDisplay(f prettyFlag, defaultString ...string) string {
usage := f.GetUsage()
if len(defaultString) > 0 {
usage += fmt.Sprintf(" (default: %v)", defaultString[0])
}
- // We pad the the shortDisplay on the right with enough spaces to equal
- // the longest flag's display
+ // We pad the shortDisplay on the right with enough spaces to equal the
+ // longest flag's display
shortDisp := shortDisplay(f)
length := utf8.RuneCountInString(shortDisp)
shortDisp += strings.Repeat(" ", maxShortDisplay-length)
@@ -120,41 +119,58 @@ func longDisplay(f prettyFlag, defaultString ...string) string {
return indent + shortDisp + indent + wrapText(usage, flagPaddingLength)
}
-// Regex that determines if we are starting an ordered list
-var listRegex = regexp.MustCompile(`^\([\d]+\)$`)
-
// Takes an input string text, and wraps the text so that each line begins with
// padding spaces (except for the first line), ends with a newline (except the
// last line), and each line has length less than lineLength. If the text
-// contains a word which is too long, that word gets its own line.
+// contains a word which is too long, that word gets its own line. Paragraphs
+// and "code blocks" are preserved.
func wrapText(text string, padding int) string {
// We use a buffer to format the wrapped text so we get O(n) runtime
var buffer bytes.Buffer
- spaceLeft := 0
- maxTextLen := lineLength - padding
+ filled := 0
delimiter := strings.Repeat(" ", padding)
- for i, word := range strings.Fields(text) {
- wordLen := utf8.RuneCountInString(word)
- switch {
- case i == 0:
- // No delimiter for the first line
- buffer.WriteString(word)
- spaceLeft = maxTextLen - wordLen
- case listRegex.Match([]byte(word)):
- // Add an additional line to separate list items.
- buffer.WriteString("\n")
- fallthrough
- case wordLen+1 > spaceLeft:
- // If no room left, write the word on the next line.
+
+ for _, line := range strings.Split(text, "\n") {
+ words := strings.Fields(line)
+
+ // Preserve empty lines (paragraph separators).
+ if len(words) == 0 {
+ if filled != 0 {
+ buffer.WriteString("\n")
+ }
buffer.WriteString("\n")
- buffer.WriteString(delimiter)
- buffer.WriteString(word)
- spaceLeft = maxTextLen - wordLen
- default:
- // Write word on this line
- buffer.WriteByte(' ')
+ filled = 0
+ continue
+ }
+
+ codeBlock := (words[0] == ">")
+ if codeBlock {
+ words[0] = " "
+ if filled != 0 {
+ buffer.WriteString("\n")
+ filled = 0
+ }
+ }
+ for _, word := range words {
+ wordLen := utf8.RuneCountInString(word)
+ // Write a newline if needed.
+ if filled != 0 && filled+1+wordLen > lineLength && !codeBlock {
+ buffer.WriteString("\n")
+ filled = 0
+ }
+ // Write a delimiter or space if needed.
+ if filled == 0 {
+ if buffer.Len() != 0 {
+ buffer.WriteString(delimiter)
+ }
+ filled += padding
+ } else {
+ buffer.WriteByte(' ')
+ filled++
+ }
+ // Write the word.
buffer.WriteString(word)
- spaceLeft -= 1 + wordLen
+ filled += wordLen
}
}
diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go
index d6162f6..93f97de 100644
--- a/cmd/fscrypt/fscrypt.go
+++ b/cmd/fscrypt/fscrypt.go
@@ -22,54 +22,49 @@
fscrypt is a command line tool for managing linux filesystem encryption.
*/
-// +build linux,cgo
-
package main
import (
"fmt"
- "io/ioutil"
+ "io"
"log"
"os"
- "time"
"github.com/urfave/cli"
-)
-var (
- // Current version of the program (set by Makefile)
- version string
- // Formatted build time (set by Makefile)
- buildTime string
- // Authors to display in the info command
- Authors = []cli.Author{{
- Name: "Joe Richey",
- Email: "joerichey@google.com",
- }}
+ "github.com/google/fscrypt/actions"
+ "github.com/google/fscrypt/filesystem"
)
+// Current version of the program (set by Makefile)
+var version string
+
func main() {
cli.AppHelpTemplate = appHelpTemplate
cli.CommandHelpTemplate = commandHelpTemplate
cli.SubcommandHelpTemplate = subcommandHelpTemplate
+ if conffile := os.Getenv("FSCRYPT_CONF"); conffile != "" {
+ actions.ConfigFileLocation = conffile
+ }
+ if rootmnt := os.Getenv("FSCRYPT_ROOT_MNT"); rootmnt != "" {
+ actions.LoginProtectorMountpoint = rootmnt
+ }
+ if consistent := os.Getenv("FSCRYPT_CONSISTENT_OUTPUT"); consistent == "1" {
+ filesystem.SortDescriptorsByLastMtime = true
+ }
+
// Create our command line application
app := cli.NewApp()
app.Usage = shortUsage
- app.Authors = Authors
- app.Copyright = apache2GoogleCopyright
- // Grab the version and compilation time passed in from the Makefile.
+ // Grab the version passed in from the Makefile.
app.Version = version
- app.Compiled, _ = time.Parse(time.UnixDate, buildTime)
app.OnUsageError = onUsageError
// Setup global flags
cli.HelpFlag = helpFlag
cli.VersionFlag = versionFlag
- cli.VersionPrinter = func(c *cli.Context) {
- cli.HelpPrinter(c.App.Writer, versionInfoTemplate, c.App)
- }
app.Flags = universalFlags
// We hide the help subcommand so that "fscrypt <command> --help" works
@@ -78,7 +73,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])
}
@@ -87,7 +82,7 @@ func main() {
}
// setupCommand performs some common setup for each command. This includes
-// hiding the help, formating the description, adding in the necessary
+// hiding the help, formatting the description, adding in the necessary
// flags, setting up error handlers, etc... Note that the command is modified
// in place and its subcommands are also setup.
func setupCommand(command *cli.Command) {
@@ -104,7 +99,7 @@ func setupCommand(command *cli.Command) {
if len(command.Subcommands) == 0 {
command.Before = setupBefore
} else {
- // Cleanup subcommands (if applicable)
+ // Setup subcommands (if applicable)
for i := range command.Subcommands {
setupCommand(&command.Subcommands[i])
}
@@ -114,11 +109,9 @@ func setupCommand(command *cli.Command) {
// setupBefore makes sure our logs, errors, and output are going to the correct
// io.Writers and that we haven't over-specified our flags. We only print the
// logs when using verbose, and only print normal stuff when not using quiet.
-// When running with sudo, this function also verifies that we have the proper
-// keyring linkage enabled.
func setupBefore(c *cli.Context) error {
- log.SetOutput(ioutil.Discard)
- c.App.Writer = ioutil.Discard
+ log.SetOutput(io.Discard)
+ c.App.Writer = io.Discard
if verboseFlag.Value {
log.SetOutput(os.Stdout)
diff --git a/cmd/fscrypt/fscrypt_bash_completion b/cmd/fscrypt/fscrypt_bash_completion
new file mode 100644
index 0000000..110d2d4
--- /dev/null
+++ b/cmd/fscrypt/fscrypt_bash_completion
@@ -0,0 +1,332 @@
+# fscrypt_bash_completion
+#
+# Copyright 2017 Google Inc.
+# Author: Henry-Joseph Audéoud (h.audeoud@gmail.com)
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+#
+# bash completion scripts require exercising some unusual shell script
+# features/quirks, so we have to disable some shellcheck warnings:
+#
+# Disable SC2016 ("Expressions don't expand in single quotes, use double quotes
+# for that") because the 'compgen' built-in expands the argument passed to -W,
+# so that argument *must* be single-quoted to avoid command injection.
+# shellcheck disable=SC2016
+#
+# Disable SC2034 ("{Variable} appears unused. Verify use (or export if used
+# externally)") because of the single quoting mentioned above as well as the
+# fact that we have to declare "local" variables used only by a called function
+# (_init_completion()) and not by the function itself.
+# shellcheck disable=SC2034
+#
+# Disable SC2207 ("Prefer mapfile or read -a to split command output (or quote
+# to avoid splitting)") because bash completion scripts conventionally use
+# COMPREPLY=($(...)) assignments.
+# shellcheck disable=SC2207
+#
+true # To apply the above shellcheck directives to the entire file
+
+
+# Generate the completion list for possible mountpoints.
+#
+# We need to be super careful here because mountpoints can contain whitespace
+# and shell meta-characters. To avoid most problems, we do the following:
+#
+# 1.) To avoid parsing ambiguities, 'fscrypt status' replaces the space, tab,
+# newline, and backslash characters with octal escape sequences -- like
+# what /proc/self/mountinfo does. To properly process its output, we need
+# to split lines on space only (and not on other whitespace which might
+# not be escaped), and unescape these characters. Exception: we don't
+# unescape newlines, as we need to reserve newline as the separator for
+# the words passed to compgen. (This causes mountpoints containing
+# newlines to not be completed correctly, which we have to tolerate.)
+#
+# 2.) We backslash-escape all shell meta-characters, and single-quote the
+# argument passed to compgen -W. Without either step, command injection
+# would be possible. Without both steps, completions would be incorrect.
+# The list of shell meta-characters used comes from that used by the
+# completion script for umount, which has to solve this same problem.
+#
+_fscrypt_compgen_mountpoints()
+{
+ local IFS=$'\n'
+ compgen -W '$(_fscrypt_mountpoints_internal)' -- "${cur}"
+}
+
+_fscrypt_mountpoints_internal()
+{
+ fscrypt status 2>/dev/null | command awk -F " " \
+ 'substr($0, 1, 1) == "/" && $5 == "Yes" {
+ gsub(/\\040/, " ", $1)
+ gsub(/\\011/, "\t", $1)
+ gsub(/\\134/, "\\", $1)
+ gsub(/[\]\[(){}<>",:;^&!$=?`|'\''\\ \t\f\n\r\v]/, "\\\\&", $1)
+ print $1
+ }'
+}
+
+# Complete with all possible mountpoints
+_fscrypt_complete_mountpoint()
+{
+ COMPREPLY=($(_fscrypt_compgen_mountpoints))
+}
+
+
+# Output list of possible policy or protector IDs
+# $1: the mount point on which policies are looked for.
+# $2: the section (policy or protector) to retrieve
+_fscrypt_status_section()
+{
+ local section=${2^^}
+ fscrypt status "$1" 2>/dev/null | \
+ command awk '/^[[:xdigit:]]{16}/ && section == "'"$section"'" { print $1; next; }
+ { section = $1 }'
+}
+
+
+# Complete with policies or protectors
+_fscrypt_complete_policy_or_protector()
+{
+ local status_section="$1"
+ if [[ $cur = *:* ]]; then
+ # Complete with IDs of the given mountpoint
+ local mountpoint="${cur%:*}" id="${cur#*:}"
+ # Note: compgen expands the argument to -W, so it *must* be single-quoted.
+ COMPREPLY=($(compgen \
+ -W '$(_fscrypt_status_section "${mountpoint}" "${status_section}")' \
+ -- "${id}"))
+ else
+ # Complete with mountpoints, with colon and without ending space
+ COMPREPLY=($(_fscrypt_compgen_mountpoints | sed s/\$/:/))
+ compopt -o nospace
+ fi
+}
+
+
+# Complete with all arguments of that function
+_fscrypt_complete_word()
+{
+ # Note: compgen expands the argument to -W, so it *must* be single-quoted.
+ COMPREPLY=($(compgen -W '$*' -- "${cur}"))
+}
+
+
+# Complete with all arguments of that function, plus global options
+_fscrypt_complete_option()
+{
+ local additional_opts=( "$@" )
+ # Add global options, always correct
+ additional_opts+=( --verbose --quiet --help )
+ # Note: compgen expands the argument to -W, so it *must* be single-quoted.
+ COMPREPLY=($(compgen -W '${additional_opts[*]}' -- "${cur}"))
+}
+
+
+_fscrypt()
+{
+ # Initialize completion: compute some local variables to easily
+ # detect what is written on the command line. -s is for splitting
+ # long options on `=`, and -n is for splitting them also on `:`
+ # (used in the protectors/policies `MOUNTPOINT:ID` forms).
+ #
+ # `split` is set by `_init_completion -s`, we must declare it local
+ # even if we don't use it, not to modify the environment.
+ local cur prev words cword split
+ _init_completion -s -n : || return
+
+ # Complete the options with argument here, if previous word were such
+ # an option. It would be too difficult to check if they take place in
+ # the correct command (such as `fscrypt status # --key ...`)—and that
+ # is the command's job—so just complete them first.
+ case $prev in
+ --key)
+ # Any file is accepted
+ _filedir
+ return ;;
+ --name)
+ # New value, nothing to complete
+ return ;;
+ --policy|--protector|--unlock-with)
+ local p_or_p="${prev#--}"
+ [[ $p_or_p = unlock-with ]] && p_or_p=protector
+ _fscrypt_complete_policy_or_protector "${p_or_p}"
+ return ;;
+ --source)
+ # Complete with keywords
+ _fscrypt_complete_word \
+ pam_passphrase custom_passphrase raw_key
+ return ;;
+ --time)
+ # It's a time, hard to complete a number…
+ return ;;
+ --user)
+ # Complete with a user
+ COMPREPLY=($(compgen -u -- "${cur}"))
+ return ;;
+ esac
+
+ # Fetch positional arguments (i.e. subcommands)
+ local positional
+ positional=()
+ local iword
+ for ((iword = 1; iword < ${#words[@]} - 1; iword++)); do
+ [[ ${words[iword - 1]} == --@(key|name|policy|protector|unlock-with|source|time|user) ]] \
+ && continue # Argument of previous option, skip
+ [[ ${words[iword]} == -* ]] && continue # Option, skip
+ positional+=("${words[iword]}")
+ done
+
+ # If completing the first positional, complete with all possible commands
+ if [[ ${#positional[@]} == 0 ]]; then
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option
+ else
+ _fscrypt_complete_word \
+ encrypt lock metadata purge setup status unlock
+ fi
+ return
+ fi
+
+ # Complete according to that provided
+ case ${positional[0]-} in
+ encrypt) # Directory or option
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option \
+ --policy= --unlock-with= --protector= --source= \
+ --user= --name= --key= --skip-unlock --no-recovery
+ else
+ _filedir -d
+ fi ;;
+ lock) # Directory or option
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option --user= --all-users
+ else
+ _filedir -d
+ fi ;;
+ purge) # Mountpoint or options
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option --user= --force
+ else
+ _fscrypt_complete_mountpoint
+ fi ;;
+ setup) # Mountpoint or options
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option --time= --force
+ else
+ _fscrypt_complete_mountpoint
+ fi ;;
+ status) # Directory (only global options for this command)
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option
+ else
+ _filedir -d
+ fi ;;
+ unlock) # Directory or option
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option --unlock-with= --user= --key=
+ else
+ _filedir -d
+ fi ;;
+ metadata)
+ # This command has subcommands
+ if [[ ${#positional[@]} = 1 ]]; then
+ if [[ $cur = -* ]]; then
+ _fscrypt_complete_option
+ else
+ # Still no subcommand, complete with them
+ _fscrypt_complete_word \
+ add-protector-to-policy create change-passphrase \
+ destroy dump remove-protector-from-policy
+ fi
+ return
+ fi
+ # We have a subcommand, complete according to it
+ case ${positional[1]-} in
+ add-protector-to-policy) # Options only
+ _fscrypt_complete_option \
+ --protector= --policy= --unlock-with= --key=
+ ;;
+ change-passphrase) # Options only
+ _fscrypt_complete_option --protector=
+ ;;
+ destroy) # Mountpoint or option
+ if [[ $cur == -* ]]; then
+ _fscrypt_complete_option \
+ --protector= --policy= --force
+ else
+ _fscrypt_complete_mountpoint
+ fi ;;
+ dump) # Options only
+ _fscrypt_complete_option --protector= --policy=
+ ;;
+ remove-protector-from-policy) # Options only
+ _fscrypt_complete_option \
+ --protector= --policy= --force
+ ;;
+ create)
+ # This subcommand has subsubcommands
+ if [[ ${#positional[@]} = 2 ]]; then
+ if [[ $cur = -* ]]; then
+ _fscrypt_complete_option
+ else
+ # Still no subcommand, complete with them
+ _fscrypt_complete_word protector policy
+ fi
+ return
+ fi
+ # We have a subsubcommand, complete according to it
+ case ${positional[2]-} in
+ policy) # Mountpoint or option
+ if [[ $cur = -* ]]; then
+ _fscrypt_complete_option --protector= --key=
+ else
+ _fscrypt_complete_mountpoint
+ fi ;;
+ protector) # Mountpoint or option
+ if [[ $cur = -* ]]; then
+ _fscrypt_complete_option \
+ --source= --name= --key= --user=
+ else
+ _fscrypt_complete_mountpoint
+ fi ;;
+ *)
+ # Unrecognized subsubcommand… Suppose a new
+ # unknown subsubcommand and complete with
+ # global options only
+ _fscrypt_complete_option
+ ;;
+ esac
+ ;;
+ *)
+ # Unrecognized subcommand… Suppose a new unknown
+ # subcommand and complete with global options only
+ _fscrypt_complete_option
+ ;;
+ esac
+ ;;
+ *)
+ # Unrecognized command… Suppose a new unknown command and
+ # complete with global options only
+ _fscrypt_complete_option
+ ;;
+ esac
+
+ # When the sole offered completion is --*=, do not put a space after
+ # the equal sign as we wait for the argument value.
+ [[ ${#COMPREPLY[@]} == 1 ]] && [[ ${COMPREPLY[0]} == "--"*"=" ]] \
+ && compopt -o nospace
+} &&
+ complete -F _fscrypt fscrypt
+
+# ex: filetype=bash
diff --git a/cmd/fscrypt/keys.go b/cmd/fscrypt/keys.go
index 872ca2a..b57c01d 100644
--- a/cmd/fscrypt/keys.go
+++ b/cmd/fscrypt/keys.go
@@ -22,13 +22,14 @@
package main
import (
+ "bufio"
"fmt"
"io"
"log"
"os"
"github.com/pkg/errors"
- "golang.org/x/crypto/ssh/terminal"
+ "golang.org/x/term"
"github.com/google/fscrypt/actions"
"github.com/google/fscrypt/crypto"
@@ -55,14 +56,14 @@ var (
// struct is empty as the reader needs to maintain no internal state.
type passphraseReader struct{}
-// Read gets input from the terminal until a newline is encountered. This read
-// should be called with the maximum buffer size for the passphrase.
+// Read gets input from the terminal until a newline is encountered or the given
+// buffer is full.
func (p passphraseReader) Read(buf []byte) (int, error) {
// We read one byte at a time to handle backspaces
position := 0
for {
if position == len(buf) {
- return position, ErrMaxPassphrase
+ return position, nil
}
if _, err := io.ReadFull(os.Stdin, buf[position:position+1]); err != nil {
return position, err
@@ -86,25 +87,53 @@ func (p passphraseReader) Read(buf []byte) (int, error) {
// passphrase into a key. If we are not reading from a terminal, just read into
// the passphrase into the key normally.
func getPassphraseKey(prompt string) (*crypto.Key, error) {
- if !quietFlag.Value {
- fmt.Print(prompt)
- }
// Only disable echo if stdin is actually a terminal.
- if terminal.IsTerminal(stdinFd) {
- state, err := terminal.MakeRaw(stdinFd)
+ if term.IsTerminal(stdinFd) {
+ state, err := term.MakeRaw(stdinFd)
if err != nil {
return nil, err
}
defer func() {
- terminal.Restore(stdinFd, state)
+ term.Restore(stdinFd, state)
fmt.Println() // To align input
}()
}
+ if !quietFlag.Value {
+ fmt.Print(prompt)
+ }
+
return crypto.NewKeyFromReader(passphraseReader{})
}
+func makeRawKey(info actions.ProtectorInfo) (*crypto.Key, error) {
+ // When running non-interactively and no key was provided,
+ // try to read it from stdin
+ if keyFileFlag.Value == "" && !term.IsTerminal(stdinFd) {
+ return crypto.NewFixedLengthKeyFromReader(bufio.NewReader(os.Stdin),
+ metadata.InternalKeyLen)
+ }
+
+ prompt := fmt.Sprintf("Enter key file for protector %q: ", info.Name())
+ // Raw keys use a file containing the key data.
+ file, err := promptForKeyFile(prompt)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ fileInfo, err := file.Stat()
+ if err != nil {
+ return nil, err
+ }
+
+ if fileInfo.Size() != metadata.InternalKeyLen {
+ return nil, errors.Wrap(ErrKeyFileLength, file.Name())
+ }
+ return crypto.NewFixedLengthKeyFromReader(file, metadata.InternalKeyLen)
+}
+
// makeKeyFunc creates an actions.KeyFunc. This function customizes the KeyFunc
// to whether or not it supports retrying, whether it confirms the passphrase,
// and custom prefix for printing (if any).
@@ -178,23 +207,7 @@ func makeKeyFunc(supportRetry, shouldConfirm bool, prefix string) actions.KeyFun
if prefix != "" {
return nil, ErrNotPassphrase
}
- prompt := fmt.Sprintf("Enter key file for protector %q: ", info.Name())
- // Raw keys use a file containing the key data.
- file, err := promptForKeyFile(prompt)
- if err != nil {
- return nil, err
- }
- defer file.Close()
-
- fileInfo, err := file.Stat()
- if err != nil {
- return nil, err
- }
-
- if fileInfo.Size() != metadata.InternalKeyLen {
- return nil, errors.Wrap(ErrKeyFileLength, file.Name())
- }
- return crypto.NewFixedLengthKeyFromReader(file, metadata.InternalKeyLen)
+ return makeRawKey(info)
default:
return nil, ErrInvalidSource
diff --git a/cmd/fscrypt/prompt.go b/cmd/fscrypt/prompt.go
index 0031e8f..d34a18a 100644
--- a/cmd/fscrypt/prompt.go
+++ b/cmd/fscrypt/prompt.go
@@ -90,7 +90,7 @@ func askConfirmation(question string, defaultChoice bool, warning string) error
// Defaults of "no" require forcing.
if !defaultChoice {
if quietFlag.Value {
- return ErrNoDesctructiveOps
+ return ErrNoDestructiveOps
}
}
@@ -185,7 +185,7 @@ func promptForSource(ctx *actions.Context) error {
}
// We print all the sources with their number, description, and name.
- fmt.Println("Your data can be protected with one of the following sources:")
+ fmt.Println("The following protector sources are available:")
for idx := 1; idx < len(metadata.SourceType_value); idx++ {
source := metadata.SourceType(idx)
description := sourceDescriptions[source]
@@ -282,7 +282,8 @@ func promptForProtector(options []*actions.ProtectorOption) (int, error) {
}
if numLoadErrors > 0 {
- fmt.Print(wrapText("NOTE: %d of the %d protectors failed to load. "+loadHelpText, 0))
+ loadWarning := fmt.Sprintf("NOTE: %d of the %d protectors failed to load. ", numLoadErrors, numOptions)
+ fmt.Print(wrapText(loadWarning+loadHelpText, 0) + "\n")
}
for {
@@ -318,7 +319,8 @@ func optionFn(policyDescriptor string, options []*actions.ProtectorOption) (int,
return idx, nil
}
}
- return 0, actions.ErrNotProtected
+ return 0, &actions.ErrNotProtected{PolicyDescriptor: policyDescriptor,
+ ProtectorDescriptor: protector.Descriptor()}
}
log.Printf("optionFn(%s)", policyDescriptor)
diff --git a/cmd/fscrypt/protector.go b/cmd/fscrypt/protector.go
index 32ba4ab..186ca7a 100644
--- a/cmd/fscrypt/protector.go
+++ b/cmd/fscrypt/protector.go
@@ -21,11 +21,14 @@
package main
import (
+ "fmt"
"log"
+ "os/user"
"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
@@ -36,6 +39,19 @@ func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error
return nil, err
}
log.Printf("using source: %s", ctx.Config.Source.String())
+ if ctx.Config.Source == metadata.SourceType_pam_passphrase {
+ if userFlag.Value == "" && util.IsUserRoot() {
+ return nil, ErrSpecifyUser
+ }
+ if !quietFlag.Value {
+ fmt.Print(`
+IMPORTANT: Before continuing, ensure you have properly set up your system for
+ login protectors. See
+ https://github.com/google/fscrypt#setting-up-for-login-protectors
+
+`)
+ }
+ }
name, err := promptForName(ctx)
if err != nil {
@@ -45,18 +61,24 @@ func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error
// We only want to create new login protectors on the root filesystem.
// So we make a new context if necessary.
- if ctx.Config.Source == metadata.SourceType_pam_passphrase && ctx.Mount.Path != "/" {
- log.Printf("creating login protector on %q instead of %q", "/", ctx.Mount.Path)
+ if ctx.Config.Source == metadata.SourceType_pam_passphrase &&
+ ctx.Mount.Path != actions.LoginProtectorMountpoint {
+ log.Printf("creating login protector on %q instead of %q",
+ actions.LoginProtectorMountpoint, ctx.Mount.Path)
if ctx, err = modifiedContext(ctx); err != nil {
return nil, err
}
}
- return actions.CreateProtector(ctx, name, createKeyFn)
+ var owner *user.User
+ if ctx.Config.Source == metadata.SourceType_pam_passphrase && util.IsUserRoot() {
+ owner = ctx.TargetUser
+ }
+ return actions.CreateProtector(ctx, name, createKeyFn, owner)
}
// selectExistingProtector returns a locked Protector which corresponds to an
-// options in the non-empty slice of options. Prompts for user input are used to
+// option in the non-empty slice of options. Prompts for user input are used to
// get the keys and select the option.
func selectExistingProtector(ctx *actions.Context, options []*actions.ProtectorOption) (*actions.Protector, error) {
idx, err := promptForProtector(options)
@@ -78,7 +100,7 @@ func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption,
}
// Do nothing different if we are at the root, or cannot load the root.
- if ctx.Mount.Path == "/" {
+ if ctx.Mount.Path == actions.LoginProtectorMountpoint {
return options, nil
}
if ctx, err = modifiedContext(ctx); err != nil {
@@ -111,10 +133,10 @@ func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption,
return options, nil
}
-// modifiedContext returns a copy of ctx with the mountpoint replaced by that of
-// the root filesystem.
+// modifiedContext returns a copy of ctx with the mountpoint replaced by
+// LoginProtectorMountpoint.
func modifiedContext(ctx *actions.Context) (*actions.Context, error) {
- mnt, err := filesystem.GetMount("/")
+ mnt, err := filesystem.GetMount(actions.LoginProtectorMountpoint)
if err != nil {
return nil, err
}
diff --git a/cmd/fscrypt/setup.go b/cmd/fscrypt/setup.go
index 72dfbdb..b9a16e8 100644
--- a/cmd/fscrypt/setup.go
+++ b/cmd/fscrypt/setup.go
@@ -1,5 +1,5 @@
/*
- * strings.go - File containing the functionality initializing directories and
+ * setup.go - File containing the functionality for initializing directories and
* the global config file.
*
* Copyright 2017 Google Inc.
@@ -26,6 +26,7 @@ import (
"os"
"github.com/google/fscrypt/actions"
+ "github.com/google/fscrypt/filesystem"
"github.com/google/fscrypt/util"
)
@@ -35,7 +36,7 @@ func createGlobalConfig(w io.Writer, path string) error {
return ErrMustBeRoot
}
- // Ask to create or replace the config file
+ // If the config file already exists, ask to replace it
_, err := os.Stat(path)
switch {
case err == nil:
@@ -44,14 +45,28 @@ func createGlobalConfig(w io.Writer, path string) error {
err = os.Remove(path)
}
case os.IsNotExist(err):
- err = askConfirmation(fmt.Sprintf("Create %q?", path), true, "")
+ err = nil
}
if err != nil {
return err
}
+ // v2 encryption policies are recommended, so set policy_version 2 when
+ // the kernel supports it. v2 policies are supported by upstream Linux
+ // v5.4 and later. For now we simply check the kernel version. Ideally
+ // we'd instead check whether setting a v2 policy actually works, in
+ // order to also detect backports of the kernel patches. However, that's
+ // hard because from this context (creating /etc/fscrypt.conf) we may
+ // not yet have access to a filesystem that supports encryption.
+ var policyVersion int64
+ if util.IsKernelVersionAtLeast(5, 4) {
+ fmt.Fprintln(w, "Defaulting to policy_version 2 because kernel supports it.")
+ policyVersion = 2
+ } else {
+ fmt.Fprintln(w, "Defaulting to policy_version 1 because kernel doesn't support v2.")
+ }
fmt.Fprintln(w, "Customizing passphrase hashing difficulty for this system...")
- err = actions.CreateConfigFile(timeTargetFlag.Value, legacyFlag.Value)
+ err = actions.CreateConfigFile(timeTargetFlag.Value, policyVersion)
if err != nil {
return err
}
@@ -66,13 +81,47 @@ func setupFilesystem(w io.Writer, path string) error {
if err != nil {
return err
}
+ username := ctx.TargetUser.Username
+
+ err = ctx.Mount.CheckSetup(ctx.TrustedUser)
+ if err == nil {
+ return &filesystem.ErrAlreadySetup{Mount: ctx.Mount}
+ }
+ if _, ok := err.(*filesystem.ErrNotSetup); !ok {
+ return err
+ }
- if err = ctx.Mount.Setup(); err != nil {
+ allUsers := allUsersSetupFlag.Value
+ if !allUsers {
+ thisFilesystem := "this filesystem"
+ if ctx.Mount.Path == "/" {
+ thisFilesystem = "the root filesystem"
+ }
+ prompt := fmt.Sprintf(`Allow users other than %s to create
+fscrypt metadata on %s? (See
+https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem)`,
+ username, thisFilesystem)
+ allUsers, err = askQuestion(wrapText(prompt, 0), false)
+ if err != nil {
+ return err
+ }
+ }
+ var setupMode filesystem.SetupMode
+ if allUsers {
+ setupMode = filesystem.WorldWritable
+ } else {
+ setupMode = filesystem.SingleUserWritable
+ }
+ if err = ctx.Mount.Setup(setupMode); err != nil {
return err
}
- fmt.Fprintf(w, "Metadata directories created at %q.\n", ctx.Mount.BaseDir())
- fmt.Fprintf(w, "Filesystem %q (%s) ready for use with %s encryption.\n",
- ctx.Mount.Path, ctx.Mount.Device, ctx.Mount.Filesystem)
+ if allUsers {
+ fmt.Fprintf(w, "Metadata directories created at %q, writable by everyone.\n",
+ ctx.Mount.BaseDir())
+ } else {
+ fmt.Fprintf(w, "Metadata directories created at %q, writable by %s only.\n",
+ ctx.Mount.BaseDir(), username)
+ }
return nil
}
diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go
index 1465a4e..bc8f1ee 100644
--- a/cmd/fscrypt/status.go
+++ b/cmd/fscrypt/status.go
@@ -27,11 +27,9 @@ import (
"strings"
"text/tabwriter"
- "github.com/pkg/errors"
-
"github.com/google/fscrypt/actions"
"github.com/google/fscrypt/filesystem"
- "github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/keyring"
)
// Creates a writer which correctly aligns tabs with the specified header.
@@ -45,12 +43,13 @@ func makeTableWriter(w io.Writer, header string) *tabwriter.Writer {
// encryptionStatus will be printed in the ENCRYPTION column. An empty string
// indicates the filesystem should not be printed.
func encryptionStatus(err error) string {
- switch errors.Cause(err) {
- case nil:
+ if err == nil {
return "supported"
- case metadata.ErrEncryptionNotEnabled:
+ }
+ switch err.(type) {
+ case *filesystem.ErrEncryptionNotEnabled:
return "not enabled"
- case metadata.ErrEncryptionNotSupported:
+ case *filesystem.ErrEncryptionNotSupported:
return "not supported"
default:
// Unknown error regarding support
@@ -65,7 +64,31 @@ func yesNoString(b bool) string {
return "No"
}
-// writeGlobalStatus prints all the filesystem that use (or could use) fscrypt.
+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 or are
+ // unlocked by another user, we'll get KeyAbsent. If we have a
+ // directory path, use a heuristic to try to detect these cases.
+ if status == keyring.KeyAbsent && policy.NeedsUserKeyring() &&
+ path != "" && isDirUnlockedHeuristic(path) {
+ return "Partially (incompletely locked, or unlocked by another user)"
+ }
+
+ switch status {
+ 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()
if err != nil {
@@ -78,7 +101,7 @@ func writeGlobalStatus(w io.Writer) error {
t := makeTableWriter(w, "MOUNTPOINT\tDEVICE\tFILESYSTEM\tENCRYPTION\tFSCRYPT")
for _, mount := range mounts {
// Only print mountpoints backed by devices or using fscrypt.
- usingFscrypt := mount.CheckSetup() == nil
+ usingFscrypt := mount.CheckSetup(nil) == nil
if !usingFscrypt && mount.Device == "" {
continue
}
@@ -91,7 +114,10 @@ func writeGlobalStatus(w io.Writer) error {
continue
}
- fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\n", mount.Path, mount.Device, mount.Filesystem,
+ fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\n",
+ filesystem.EscapeString(mount.Path),
+ filesystem.EscapeString(mount.Device),
+ filesystem.EscapeString(mount.FilesystemType),
supportString, yesNoString(usingFscrypt))
if supportErr == nil {
@@ -134,13 +160,27 @@ func writeFilesystemStatus(w io.Writer, ctx *actions.Context) error {
return err
}
- policyDescriptors, err := ctx.Mount.ListPolicies()
+ policyDescriptors, err := ctx.Mount.ListPolicies(ctx.TrustedUser)
if err != nil {
return err
}
- fmt.Fprintf(w, "%s filesystem %q has %s and %s\n\n", ctx.Mount.Filesystem, ctx.Mount.Path,
- pluralize(len(options), "protector"), pluralize(len(policyDescriptors), "policy"))
+ filterDescription := ""
+ if ctx.TrustedUser != nil {
+ filterDescription = fmt.Sprintf(" (only including ones owned by %s or root)", ctx.TrustedUser.Username)
+ }
+ fmt.Fprintf(w, "%s filesystem %q has %s and %s%s.\n", ctx.Mount.FilesystemType,
+ ctx.Mount.Path, pluralize(len(options), "protector"),
+ pluralize(len(policyDescriptors), "policy"), filterDescription)
+ if setupMode, user, err := ctx.Mount.GetSetupMode(); err == nil {
+ switch setupMode {
+ case filesystem.WorldWritable:
+ fmt.Fprintf(w, "All users can create fscrypt metadata on this filesystem.\n")
+ case filesystem.SingleUserWritable:
+ fmt.Fprintf(w, "Only %s can create fscrypt metadata on this filesystem.\n", user.Username)
+ }
+ }
+ fmt.Fprintf(w, "\n")
if len(options) > 0 {
writeOptions(w, options)
@@ -159,7 +199,8 @@ 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()
@@ -178,7 +219,8 @@ func writePathStatus(w io.Writer, path string) error {
fmt.Fprintf(w, "%q is encrypted with fscrypt.\n", path)
fmt.Fprintln(w)
fmt.Fprintf(w, "Policy: %s\n", policy.Descriptor())
- fmt.Fprintf(w, "Unlocked: %s\n", yesNoString(policy.IsProvisioned()))
+ fmt.Fprintf(w, "Options: %s\n", policy.Options())
+ fmt.Fprintf(w, "Unlocked: %s\n", policyUnlockedStatus(policy, path))
fmt.Fprintln(w)
options := policy.ProtectorOptions()
diff --git a/cmd/fscrypt/strings.go b/cmd/fscrypt/strings.go
index fb79c38..cd51968 100644
--- a/cmd/fscrypt/strings.go
+++ b/cmd/fscrypt/strings.go
@@ -25,24 +25,7 @@ import (
"strings"
)
-// Global application strings
-const (
- shortUsage = "A tool for managing Linux filesystem encryption"
-
- apache2GoogleCopyright = `Copyright 2017 Google, Inc.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.`
-)
+const shortUsage = "A tool for managing Linux native filesystem encryption"
// Argument usage strings
const (
@@ -103,23 +86,6 @@ Options:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}`
-
- // Additional info, used with "fscrypt version"
- versionInfoTemplate = `{{.HelpName}} - {{.Usage}}
-
-{{if .Version}}Version:
-` + indent + `{{.Version}}
-
-{{end}}{{if .Compiled}}Compiled:
-` + indent + `{{.Compiled}}
-
-{{end}}{{if len .Authors}}Author{{with $length := len .Authors}}{{if ne 1 $length}}s{{end}}{{end}}:{{range .Authors}}
-` + indent + `{{.}}{{end}}
-
-{{end}}{{if .Copyright}}Copyright:
-` + indent + `{{.Copyright}}
-
-{{end}}`
)
// Add words to this map to have pluralize support them.
@@ -130,7 +96,7 @@ var plurals = map[string]string{
"policy": "policies",
}
-// pluralize prints our the correct pluralization of a work along with the
+// pluralize prints out the correct pluralization of a word along with the
// specified count. This means pluralize(1, "policy") = "1 policy" but
// pluralize(2, "policy") = "2 policies"
func pluralize(count int, word string) string {