/*
 * policy.go - Functions for getting and setting policies on a specified
 * directory or file.
 *
 * 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 metadata

import (
	"encoding/hex"
	"fmt"
	"log"
	"math"
	"os"
	"os/user"
	"strconv"
	"unsafe"

	"github.com/pkg/errors"
	"golang.org/x/sys/unix"

	"github.com/google/fscrypt/util"
)

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 indicates that encryption is not supported on
	// the given filesystem, but there is a way to enable it.
	ErrEncryptionNotEnabled = errors.New("encryption not enabled")
)

// 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)
}

// 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 policyIoctl(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
	}
	return errno
}

// Maps EncryptionOptions.Padding <-> FSCRYPT_POLICY_FLAGS
var (
	paddingArray = []int64{4, 8, 16, 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 {
		return nil, err
	}
	defer file.Close()

	// 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 = policyIoctl(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 = policyIoctl(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])
	}
}

// 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
	}
	// Whitelist the modes that take a 24+ byte IV (enough room for the per-file nonce)
	return options.Contents == EncryptionOptions_Adiantum
}

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 of %d was not found", options.Padding)
	}
	if shouldUseDirectKeyFlag(options) {
		flags |= unix.FSCRYPT_POLICY_FLAG_DIRECT_KEY
	}
	return uint8(flags)
}

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 policyIoctl(file, unix.FS_IOC_SET_ENCRYPTION_POLICY, 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 policyIoctl(file, unix.FS_IOC_SET_ENCRYPTION_POLICY, unsafe.Pointer(&policy))
}

// SetPolicy sets up the specified directory to be encrypted with the specified
// policy. Returns an error if we cannot set the policy for any reason (not a
// directory, invalid options or KeyDescriptor, etc).
func SetPolicy(path string, data *PolicyData) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	if err = data.CheckValidity(); err != nil {
		return errors.Wrap(err, "invalid policy")
	}

	descriptorBytes, err := hex.DecodeString(data.KeyDescriptor)
	if err != nil {
		return errors.New("invalid key descriptor: " + data.KeyDescriptor)
	}

	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)
	}
	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.
		if info, statErr := os.Stat(path); statErr != nil || !info.IsDir() {
			// Checking if the path is not a directory
			err = unix.ENOTDIR
		} else if _, policyErr := GetPolicy(path); policyErr == nil {
			// Checking if a policy is already set on this directory
			err = unix.EEXIST
		}
	}
	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
// support filesystem encryption. This can be for many reasons including an
// incompatible kernel or filesystem or not enabling the right feature flags.
func CheckSupport(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	// On supported directories, giving a bad policy will return EINVAL
	badPolicy := unix.FscryptPolicyV1{
		Version:                   math.MaxUint8,
		Contents_encryption_mode:  math.MaxUint8,
		Filenames_encryption_mode: math.MaxUint8,
		Flags:                     math.MaxUint8,
	}

	err = policyIoctl(file, unix.FS_IOC_SET_ENCRYPTION_POLICY, 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 errors.Wrapf(err, "unexpected error checking for encryption support on filesystem %q", path)
}
