/* * errors.go - File which contains common error handling code for fscrypt * commands. This includes handling for bad usage, invalid commands, and errors * from the other packages * * Copyright 2017 Google Inc. * Author: Joe Richey (joerichey@google.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. */ package main import ( "bytes" "fmt" "os" "path/filepath" "unicode/utf8" "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/util" ) // failureExitCode is the value fscrypt will return on failure. const failureExitCode = 1 // Various errors used for the top level user interface var ( ErrCanceled = errors.New("operation canceled") 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") ErrWrongKey = errors.New("incorrect key provided") ErrSpecifyKeyFile = errors.New("no key file specified") 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") 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 fmt.Sprintf(`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. func getFullName(c *cli.Context) string { if c.Command.HelpName != "" { return c.Command.HelpName } 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." } } 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".` } switch errors.Cause(err) { 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 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)) case ErrSpecifyKeyFile: return fmt.Sprintf("Use %s to specify a key file.", shortDisplay(keyFileFlag)) 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 user's keyring and use a normal user's login passphrase as a protector (so the corresponding files will be accessible for that user). This can be done with %s. To use the root user's keyring or passphrase, use --%s=root.`, shortDisplay(userFlag), userFlag.GetName()) case ErrAllLoadsFailed: return loadHelpText default: return "" } } // newExitError creates a new error for a given context and normal error. The // 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 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) } return cli.NewExitError(message, failureExitCode) } // 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 } func (u *usageError) Error() string { return fmt.Sprintf("%s: %s", getFullName(u.c), u.message) } // We get the help to print after the error by having it run right before the // application exits. This is very nasty, but there isn't a better way to do it // with the constraints of urfave/cli. func (u *usageError) ExitCode() int { // Redirect help output to a buffer, so we can customize it. buf := new(bytes.Buffer) oldWriter := u.c.App.Writer u.c.App.Writer = buf // Get the appropriate help if getFullName(u.c) == filepath.Base(os.Args[0]) { cli.ShowAppHelp(u.c) } else { cli.ShowCommandHelp(u.c, u.c.Command.Name) } // Remove first line from help and print it out buf.ReadBytes('\n') buf.WriteTo(oldWriter) u.c.App.Writer = oldWriter return failureExitCode } // expectedArgsErr creates a usage error for the incorrect number of arguments // being specified. atMost should be true only if any number of arguments from 0 // to expectedArgs would be acceptable. func expectedArgsErr(c *cli.Context, expectedArgs int, atMost bool) error { message := "expected " if atMost { message += "at most " } message += fmt.Sprintf("%s, got %s", pluralize(expectedArgs, "argument"), pluralize(c.NArg(), "argument")) return &usageError{c, message} } // onUsageError is a function handler for the application and each command. func onUsageError(c *cli.Context, err error, _ bool) error { return &usageError{c, err.Error()} } // checkRequiredFlags makes sure that all of the specified string flags have // been given nonempty values. Returns a usage error on failure. func checkRequiredFlags(c *cli.Context, flags []*stringFlag) error { for _, flag := range flags { if flag.Value == "" { message := fmt.Sprintf("required flag %s not provided", shortDisplay(flag)) return &usageError{c, message} } } return nil }