diff options
Diffstat (limited to 'metadata/policy.go')
| -rw-r--r-- | metadata/policy.go | 325 |
1 files changed, 254 insertions, 71 deletions
diff --git a/metadata/policy.go b/metadata/policy.go index 533d48a..fe6c38f 100644 --- a/metadata/policy.go +++ b/metadata/policy.go @@ -22,9 +22,13 @@ package metadata import ( "encoding/hex" + "fmt" "log" "math" "os" + "os/user" + "strconv" + "syscall" "unsafe" "github.com/pkg/errors" @@ -33,80 +37,248 @@ import ( "github.com/google/fscrypt/util" ) -// Encryption specific errors var ( + // ErrEncryptionNotSupported indicates that encryption is not supported + // on the given filesystem, and there is no way to enable it. ErrEncryptionNotSupported = errors.New("encryption not supported") - ErrEncryptionNotEnabled = errors.New("encryption not enabled") - ErrNotEncrypted = errors.New("file or directory not encrypted") - ErrEncrypted = errors.New("file or directory already encrypted") - ErrBadEncryptionOptions = util.SystemError("invalid encryption options provided") + + // ErrEncryptionNotEnabled indicates that encryption is not supported on + // the given filesystem, but there is a way to enable it. + ErrEncryptionNotEnabled = errors.New("encryption not enabled") ) -// policyIoctl is a wrapper for the ioctl syscall. It passes the correct -// pointers and file descriptors to the IOCTL syscall. This function also takes -// some of the unclear errors returned by the syscall and translates then into -// more specific error strings. -func policyIoctl(file *os.File, request uintptr, policy *unix.FscryptPolicy) error { - // The returned errno value can sometimes give strange errors, so we - // return encryption specific errors. - _, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), request, uintptr(unsafe.Pointer(policy))) - switch errno { - case 0: +// ErrAlreadyEncrypted indicates that the path is already encrypted. +type ErrAlreadyEncrypted struct { + Path string +} + +func (err *ErrAlreadyEncrypted) Error() string { + return fmt.Sprintf("file or directory %q is already encrypted", err.Path) +} + +// ErrBadEncryptionOptions indicates that unsupported encryption options were given. +type ErrBadEncryptionOptions struct { + Path string + Options *EncryptionOptions +} + +func (err *ErrBadEncryptionOptions) Error() string { + return fmt.Sprintf(`cannot encrypt %q because the kernel doesn't support the requested encryption options. + + The options are %s`, err.Path, err.Options) +} + +// ErrDirectoryNotOwned indicates a directory can't be encrypted because it's +// owned by another user. +type ErrDirectoryNotOwned struct { + Path string + Owner uint32 +} + +func (err *ErrDirectoryNotOwned) Error() string { + owner := strconv.Itoa(int(err.Owner)) + if u, e := user.LookupId(owner); e == nil && u.Username != "" { + owner = u.Username + } + return fmt.Sprintf(`cannot encrypt %q because it's owned by another user (%s). + + Encryption can only be enabled on a directory you own, even if you have + write access to the directory.`, err.Path, owner) +} + +// ErrLockedRegularFile indicates that the path is a locked regular file. +type ErrLockedRegularFile struct { + Path string +} + +func (err *ErrLockedRegularFile) Error() string { + return fmt.Sprintf("cannot operate on locked regular file %q", err.Path) +} + +// ErrNotEncrypted indicates that the path is not encrypted. +type ErrNotEncrypted struct { + Path string +} + +func (err *ErrNotEncrypted) Error() string { + return fmt.Sprintf("file or directory %q is not encrypted", err.Path) +} + +func getPolicyIoctl(file *os.File, request uintptr, arg unsafe.Pointer) error { + _, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), request, uintptr(arg)) + if errno == 0 { return nil - case unix.ENOTTY: - return ErrEncryptionNotSupported - case unix.EOPNOTSUPP: - return ErrEncryptionNotEnabled - case unix.ENODATA, unix.ENOENT: - // ENOENT was returned instead of ENODATA on some filesystems before v4.11. - return ErrNotEncrypted - case unix.EEXIST: - // EINVAL was returned instead of EEXIST on some filesystems before v4.11. - return ErrEncrypted - default: + } + return errno +} + +func setPolicy(file *os.File, arg unsafe.Pointer) error { + _, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), unix.FS_IOC_SET_ENCRYPTION_POLICY, uintptr(arg)) + if errno != 0 { return errno } + + if err := file.Sync(); err != nil { + return err + } + + return nil } -// Maps EncryptionOptions.Padding <-> FscryptPolicy.Flags +// Maps EncryptionOptions.Padding <-> FSCRYPT_POLICY_FLAGS var ( paddingArray = []int64{4, 8, 16, 32} - flagsArray = []int64{unix.FS_POLICY_FLAGS_PAD_4, unix.FS_POLICY_FLAGS_PAD_8, - unix.FS_POLICY_FLAGS_PAD_16, unix.FS_POLICY_FLAGS_PAD_32} + flagsArray = []int64{unix.FSCRYPT_POLICY_FLAGS_PAD_4, unix.FSCRYPT_POLICY_FLAGS_PAD_8, + unix.FSCRYPT_POLICY_FLAGS_PAD_16, unix.FSCRYPT_POLICY_FLAGS_PAD_32} ) +// flagsToPadding returns the amount of padding specified in the policy flags. +func flagsToPadding(flags uint8) int64 { + paddingFlag := int64(flags & unix.FS_POLICY_FLAGS_PAD_MASK) + + // This lookup should always succeed + padding, ok := util.Lookup(paddingFlag, flagsArray, paddingArray) + if !ok { + log.Panicf("padding flag of %x not found", paddingFlag) + } + return padding +} + +func buildV1PolicyData(policy *unix.FscryptPolicyV1) *PolicyData { + return &PolicyData{ + KeyDescriptor: hex.EncodeToString(policy.Master_key_descriptor[:]), + Options: &EncryptionOptions{ + Padding: flagsToPadding(policy.Flags), + Contents: EncryptionOptions_Mode(policy.Contents_encryption_mode), + Filenames: EncryptionOptions_Mode(policy.Filenames_encryption_mode), + PolicyVersion: 1, + }, + } +} + +func buildV2PolicyData(policy *unix.FscryptPolicyV2) *PolicyData { + return &PolicyData{ + KeyDescriptor: hex.EncodeToString(policy.Master_key_identifier[:]), + Options: &EncryptionOptions{ + Padding: flagsToPadding(policy.Flags), + Contents: EncryptionOptions_Mode(policy.Contents_encryption_mode), + Filenames: EncryptionOptions_Mode(policy.Filenames_encryption_mode), + PolicyVersion: 2, + }, + } +} + // GetPolicy returns the Policy data for the given directory or file (includes // the KeyDescriptor and the encryption options). Returns an error if the // path is not encrypted or the policy couldn't be retrieved. func GetPolicy(path string) (*PolicyData, error) { file, err := os.Open(path) if err != nil { + if err.(*os.PathError).Err == syscall.ENOKEY { + return nil, &ErrLockedRegularFile{path} + } return nil, err } defer file.Close() - var policy unix.FscryptPolicy - if err := policyIoctl(file, unix.FS_IOC_GET_ENCRYPTION_POLICY, &policy); err != nil { - return nil, errors.Wrapf(err, "get encryption policy %s", path) + // First try the new version of the ioctl. This works for both v1 and v2 policies. + var arg unix.FscryptGetPolicyExArg + arg.Size = uint64(unsafe.Sizeof(arg.Policy)) + policyPtr := util.Ptr(arg.Policy[:]) + err = getPolicyIoctl(file, unix.FS_IOC_GET_ENCRYPTION_POLICY_EX, unsafe.Pointer(&arg)) + if err == unix.ENOTTY { + // Fall back to the old version of the ioctl. This works for v1 policies only. + err = getPolicyIoctl(file, unix.FS_IOC_GET_ENCRYPTION_POLICY, policyPtr) + arg.Size = uint64(unsafe.Sizeof(unix.FscryptPolicyV1{})) } + switch err { + case nil: + break + case unix.ENOTTY: + return nil, ErrEncryptionNotSupported + case unix.EOPNOTSUPP: + return nil, ErrEncryptionNotEnabled + case unix.ENODATA, unix.ENOENT: + // ENOENT was returned instead of ENODATA on some filesystems before v4.11. + return nil, &ErrNotEncrypted{path} + default: + return nil, errors.Wrapf(err, "failed to get encryption policy of %q", path) + } + switch arg.Policy[0] { // arg.policy.version + case unix.FSCRYPT_POLICY_V1: + if arg.Size != uint64(unsafe.Sizeof(unix.FscryptPolicyV1{})) { + // should never happen + return nil, errors.New("unexpected size for v1 policy") + } + return buildV1PolicyData((*unix.FscryptPolicyV1)(policyPtr)), nil + case unix.FSCRYPT_POLICY_V2: + if arg.Size != uint64(unsafe.Sizeof(unix.FscryptPolicyV2{})) { + // should never happen + return nil, errors.New("unexpected size for v2 policy") + } + return buildV2PolicyData((*unix.FscryptPolicyV2)(policyPtr)), nil + default: + return nil, errors.Errorf("unsupported encryption policy version [%d]", + arg.Policy[0]) + } +} - // Convert the padding flag into an amount of padding - paddingFlag := int64(policy.Flags & unix.FS_POLICY_FLAGS_PAD_MASK) +// For improved performance, use the DIRECT_KEY flag when using ciphers that +// support it, e.g. Adiantum. It is safe because fscrypt won't reuse the key +// for any other policy. (Multiple directories with same policy are okay.) +func shouldUseDirectKeyFlag(options *EncryptionOptions) bool { + // Contents and filenames encryption modes must be the same + if options.Contents != options.Filenames { + return false + } + // Currently only Adiantum supports DIRECT_KEY. + return options.Contents == EncryptionOptions_Adiantum +} - // This lookup should always succeed - padding, ok := util.Lookup(paddingFlag, flagsArray, paddingArray) +func buildPolicyFlags(options *EncryptionOptions) uint8 { + // This lookup should always succeed (as policy is valid) + flags, ok := util.Lookup(options.Padding, paddingArray, flagsArray) if !ok { - log.Panicf("padding flag of %x not found", paddingFlag) + log.Panicf("padding of %d was not found", options.Padding) + } + if shouldUseDirectKeyFlag(options) { + flags |= unix.FSCRYPT_POLICY_FLAG_DIRECT_KEY } + return uint8(flags) +} - return &PolicyData{ - KeyDescriptor: hex.EncodeToString(policy.Master_key_descriptor[:]), - Options: &EncryptionOptions{ - Padding: padding, - Contents: EncryptionOptions_Mode(policy.Contents_encryption_mode), - Filenames: EncryptionOptions_Mode(policy.Filenames_encryption_mode), - }, - }, nil +func setV1Policy(file *os.File, options *EncryptionOptions, descriptorBytes []byte) error { + policy := unix.FscryptPolicyV1{ + Version: unix.FSCRYPT_POLICY_V1, + Contents_encryption_mode: uint8(options.Contents), + Filenames_encryption_mode: uint8(options.Filenames), + Flags: uint8(buildPolicyFlags(options)), + } + + // The descriptor should always be the correct length (as policy is valid) + if len(descriptorBytes) != unix.FSCRYPT_KEY_DESCRIPTOR_SIZE { + log.Panic("wrong descriptor size for v1 policy") + } + copy(policy.Master_key_descriptor[:], descriptorBytes) + + return setPolicy(file, unsafe.Pointer(&policy)) +} + +func setV2Policy(file *os.File, options *EncryptionOptions, descriptorBytes []byte) error { + policy := unix.FscryptPolicyV2{ + Version: unix.FSCRYPT_POLICY_V2, + Contents_encryption_mode: uint8(options.Contents), + Filenames_encryption_mode: uint8(options.Filenames), + Flags: uint8(buildPolicyFlags(options)), + } + + // The descriptor should always be the correct length (as policy is valid) + if len(descriptorBytes) != unix.FSCRYPT_KEY_IDENTIFIER_SIZE { + log.Panic("wrong descriptor size for v2 policy") + } + copy(policy.Master_key_identifier[:], descriptorBytes) + + return setPolicy(file, unsafe.Pointer(&policy)) } // SetPolicy sets up the specified directory to be encrypted with the specified @@ -119,30 +291,24 @@ func SetPolicy(path string, data *PolicyData) error { } defer file.Close() - if err := data.CheckValidity(); err != nil { + if err = data.CheckValidity(); err != nil { return errors.Wrap(err, "invalid policy") } - // This lookup should always succeed (as policy is valid) - paddingFlag, ok := util.Lookup(data.Options.Padding, paddingArray, flagsArray) - if !ok { - log.Panicf("padding of %d was not found", data.Options.Padding) - } - descriptorBytes, err := hex.DecodeString(data.KeyDescriptor) if err != nil { - return errors.New("invalid descriptor: " + data.KeyDescriptor) + return errors.New("invalid key descriptor: " + data.KeyDescriptor) } - policy := unix.FscryptPolicy{ - Version: 0, // Version must always be zero - Contents_encryption_mode: uint8(data.Options.Contents), - Filenames_encryption_mode: uint8(data.Options.Filenames), - Flags: uint8(paddingFlag), + switch data.Options.PolicyVersion { + case 1: + err = setV1Policy(file, data.Options, descriptorBytes) + case 2: + err = setV2Policy(file, data.Options, descriptorBytes) + default: + err = errors.Errorf("policy version of %d is invalid", data.Options.PolicyVersion) } - copy(policy.Master_key_descriptor[:], descriptorBytes) - - if err = policyIoctl(file, unix.FS_IOC_SET_ENCRYPTION_POLICY, &policy); err == unix.EINVAL { + if err == unix.EINVAL { // Before kernel v4.11, many different errors all caused unix.EINVAL to be returned. // We try to disambiguate this error here. This disambiguation will not always give // the correct error due to a potential race condition on path. @@ -151,14 +317,27 @@ func SetPolicy(path string, data *PolicyData) error { err = unix.ENOTDIR } else if _, policyErr := GetPolicy(path); policyErr == nil { // Checking if a policy is already set on this directory - err = ErrEncrypted - } else { - // Default to generic "bad options". - err = ErrBadEncryptionOptions + err = unix.EEXIST } } - - return errors.Wrapf(err, "set encryption policy %s", path) + switch err { + case nil: + return nil + case unix.EACCES: + var stat unix.Stat_t + if statErr := unix.Stat(path, &stat); statErr == nil && stat.Uid != uint32(os.Geteuid()) { + return &ErrDirectoryNotOwned{path, stat.Uid} + } + case unix.EEXIST: + return &ErrAlreadyEncrypted{path} + case unix.EINVAL: + return &ErrBadEncryptionOptions{path, data.Options} + case unix.ENOTTY: + return ErrEncryptionNotSupported + case unix.EOPNOTSUPP: + return ErrEncryptionNotEnabled + } + return errors.Wrapf(err, "failed to set encryption policy on %q", path) } // CheckSupport returns an error if the filesystem containing path does not @@ -172,20 +351,24 @@ func CheckSupport(path string) error { defer file.Close() // On supported directories, giving a bad policy will return EINVAL - badPolicy := unix.FscryptPolicy{ + badPolicy := unix.FscryptPolicyV1{ Version: math.MaxUint8, Contents_encryption_mode: math.MaxUint8, Filenames_encryption_mode: math.MaxUint8, - Flags: math.MaxUint8, + Flags: math.MaxUint8, } - err = policyIoctl(file, unix.FS_IOC_SET_ENCRYPTION_POLICY, &badPolicy) + err = setPolicy(file, unsafe.Pointer(&badPolicy)) switch err { case nil: log.Panicf(`FS_IOC_SET_ENCRYPTION_POLICY succeeded when it should have failed. Please open an issue, filesystem %q may be corrupted.`, path) case unix.EINVAL, unix.EACCES: return nil + case unix.ENOTTY: + return ErrEncryptionNotSupported + case unix.EOPNOTSUPP: + return ErrEncryptionNotEnabled } - return err + return errors.Wrapf(err, "unexpected error checking for encryption support on filesystem %q", path) } |