diff options
Diffstat (limited to 'metadata')
| -rw-r--r-- | metadata/config.go | 6 | ||||
| -rw-r--r-- | metadata/config_test.go | 6 | ||||
| -rw-r--r-- | metadata/metadata.pb.go | 2 | ||||
| -rw-r--r-- | metadata/metadata.proto | 2 | ||||
| -rw-r--r-- | metadata/policy.go | 166 | ||||
| -rw-r--r-- | metadata/policy_test.go | 162 |
6 files changed, 335 insertions, 9 deletions
diff --git a/metadata/config.go b/metadata/config.go index 1d73755..47b6cce 100644 --- a/metadata/config.go +++ b/metadata/config.go @@ -20,8 +20,10 @@ // Package metadata contains all of the on disk structures. // These structures are definied in meatadata.proto. The package also -// contains functions for reading and writing the Config file to disk -// giving us a config file. +// contains functions for manipulating these structures, specifically: +// * Reading and Writing the Config file to disk +// * Getting and Setting Policies for directories +// * Reasonable defaults for a Policy's EncryptionOptions package metadata //go:generate protoc --go_out=. metadata.proto diff --git a/metadata/config_test.go b/metadata/config_test.go index ecdd44f..1903785 100644 --- a/metadata/config_test.go +++ b/metadata/config_test.go @@ -32,11 +32,7 @@ var testConfig = &Config{ Parallelism: 8, }, Compatibility: "", - Options: &EncryptionOptions{ - Padding: 32, - ContentsMode: EncryptionMode_XTS, - FilenamesMode: EncryptionMode_CTS, - }, + Options: DefaultOptions, } var testConfigString = `{ diff --git a/metadata/metadata.pb.go b/metadata/metadata.pb.go index 68001e9..bf30309 100644 --- a/metadata/metadata.pb.go +++ b/metadata/metadata.pb.go @@ -62,7 +62,7 @@ func (x SourceType) String() string { } func (SourceType) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } -// Type of encryption, should match the declarations of FS_ENCRYPTION_MODE +// Type of encryption, should match the declarations of unix.FS_ENCRYPTION_MODE type EncryptionMode int32 const ( diff --git a/metadata/metadata.proto b/metadata/metadata.proto index b967407..f3103f8 100644 --- a/metadata/metadata.proto +++ b/metadata/metadata.proto @@ -58,7 +58,7 @@ message ProtectorData { WrappedKeyData wrapped_key = 7; } -// Type of encryption, should match the declarations of FS_ENCRYPTION_MODE +// Type of encryption, should match the declarations of unix.FS_ENCRYPTION_MODE enum EncryptionMode { default = 0; XTS = 1; diff --git a/metadata/policy.go b/metadata/policy.go new file mode 100644 index 0000000..ae8b869 --- /dev/null +++ b/metadata/policy.go @@ -0,0 +1,166 @@ +/* + * 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" + "errors" + "os" + "unsafe" + + "golang.org/x/sys/unix" + + "fscrypt/util" +) + +// DescriptorLen is the length of all Protector and Policy descriptors. +const DescriptorLen = 2 * unix.FS_KEY_DESCRIPTOR_SIZE + +// Encryption specific errors +var ( + ErrEncryptionNotSupported = errors.New("filesystem encryption is not supported") + ErrEncryptionDisabled = errors.New("filesystem encryption has been disabled in the kernel config") + ErrNotEncrypted = errors.New("file or directory not encrypted") + ErrEncrypted = errors.New("file or directory already encrypted") + ErrBadEncryptionOptions = errors.New("invalid encryption options provided") +) + +// policyIoctl is a wrapper for the ioctl syscall. If opens the file at the path +// and 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(path string, request uintptr, policy *unix.FscryptPolicy) error { + file, err := os.Open(path) + if err != nil { + // For PathErrors, we just want the underlying error + return util.UnderlyingError(err) + } + defer file.Close() + + // 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: + return nil + case unix.ENOTTY: + return ErrEncryptionNotSupported + case unix.EOPNOTSUPP: + return ErrEncryptionDisabled + 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 + } +} + +// DefaultOptions use the only supported encryption modes and maximum padding. +var DefaultOptions = &EncryptionOptions{ + Padding: 32, + ContentsMode: EncryptionMode_XTS, + FilenamesMode: EncryptionMode_CTS, +} + +// Maps EncryptionOptions.Padding <-> FscryptPolicy.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} +) + +// 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) { + var policy unix.FscryptPolicy + if err := policyIoctl(path, unix.FS_IOC_GET_ENCRYPTION_POLICY, &policy); err != nil { + return nil, err + } + + // Convert the padding flag into an amount of padding + paddingFlag := int64(policy.Flags & unix.FS_POLICY_FLAGS_PAD_MASK) + padding, ok := util.Lookup(paddingFlag, flagsArray, paddingArray) + if !ok { + return nil, util.SystemErrorF("invalid padding flag: %x", paddingFlag) + } + + return &PolicyData{ + KeyDescriptor: hex.EncodeToString(policy.Master_key_descriptor[:]), + Options: &EncryptionOptions{ + Padding: padding, + ContentsMode: EncryptionMode(policy.Contents_encryption_mode), + FilenamesMode: EncryptionMode(policy.Filenames_encryption_mode), + }, + }, nil +} + +// 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 { + // Convert the padding value to a flag and the policyID to a byte array + paddingFlag, ok := util.Lookup(data.Options.Padding, paddingArray, flagsArray) + if !ok { + return util.InvalidInputF("padding of %d", data.Options.Padding) + } + + if len(data.KeyDescriptor) != DescriptorLen { + return util.InvalidLengthError("policy descriptor", DescriptorLen, len(data.KeyDescriptor)) + } + + descriptorBytes, err := hex.DecodeString(data.KeyDescriptor) + if err != nil { + return util.InvalidInputF("policy descriptor of %s: %v", data.KeyDescriptor, err) + } + + policy := unix.FscryptPolicy{ + Version: 0, // Version must always be zero + Contents_encryption_mode: uint8(data.Options.ContentsMode), + Filenames_encryption_mode: uint8(data.Options.FilenamesMode), + Flags: uint8(paddingFlag), + } + copy(policy.Master_key_descriptor[:], descriptorBytes) + + if err = policyIoctl(path, unix.FS_IOC_SET_ENCRYPTION_POLICY, &policy); err != nil { + // 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 err == unix.EINVAL { + // Checking if the path is not a directory + if info, err := os.Stat(path); err != nil || !info.IsDir() { + return unix.ENOTDIR + } + // Checking if a policy is already set on this directory + if _, err := GetPolicy(path); err == nil { + return ErrEncrypted + } + // Could not get a more detailed error, return generic "bad options". + return ErrBadEncryptionOptions + } + return err + } + + return nil +} diff --git a/metadata/policy_test.go b/metadata/policy_test.go new file mode 100644 index 0000000..7f8a48b --- /dev/null +++ b/metadata/policy_test.go @@ -0,0 +1,162 @@ +/* + * policy_test.go - Tests the getting/setting of encryption policies + * + * 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 ( + "fmt" + "os" + "path/filepath" + "reflect" + "testing" +) + +const goodDescriptor = "0123456789abcdef" + +var goodPolicy = &PolicyData{ + KeyDescriptor: goodDescriptor, + Options: DefaultOptions, +} + +// Creates a temporary directory in BASE_TEST_DIR for testing. Fails if the +// base directory is not specified. +func createTestDirectory() (directory string, err error) { + baseDirectory := os.Getenv("BASE_TEST_DIR") + if s, err := os.Stat(baseDirectory); err != nil || !s.IsDir() { + return "", fmt.Errorf("invalid directory %q. Set BASE_TEST_DIR to be a valid directory", baseDirectory) + } + + directoryPath := filepath.Join(baseDirectory, "test") + return directoryPath, os.MkdirAll(directoryPath, os.ModePerm) +} + +// Makes a test directory, makes a file in the directory, and fills the file +// with data. Returns the directory name, file name, and error (if one). +func createTestFile() (directory, file string, err error) { + if directory, err = createTestDirectory(); err != nil { + return + } + // Cleanup if the file creation fails + defer func() { + if err != nil { + os.RemoveAll(directory) + } + }() + + filePath := filepath.Join(directory, "test.txt") + fileHandle, err := os.Create(filePath) + if err != nil { + return directory, filePath, err + } + defer fileHandle.Close() + + _, err = fileHandle.Write([]byte("Here is some test data!\n")) + return directory, filePath, err +} + +// Tests that we can set a policy on an empty directory +func TestSetPolicyEmptyDirectory(t *testing.T) { + directory, err := createTestDirectory() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(directory) + + if err = SetPolicy(directory, goodPolicy); err != nil { + t.Error(err) + } +} + +// Tests that we cannot set a policy on a nonempty directory +func TestSetPolicyNonemptyDirectory(t *testing.T) { + directory, _, err := createTestFile() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(directory) + + if err = SetPolicy(directory, goodPolicy); err == nil { + t.Error("should have failed to set policy on a nonempty directory") + } +} + +// Tests that we cannot set a policy on a file +func TestSetPolicyFile(t *testing.T) { + directory, file, err := createTestFile() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(directory) + + if err = SetPolicy(file, goodPolicy); err == nil { + t.Error("should have failed to set policy on a file") + } +} + +// Tests that we fail when using bad policies +func TestSetPolicyBadIDs(t *testing.T) { + // Policies that are too short, have invalid chars, or are too long + badDescriptors := []string{"123456789abcde", "xxxxxxxxxxxxxxxx", "0123456789abcdef00"} + for _, badDescriptor := range badDescriptors { + badPolicy := &PolicyData{KeyDescriptor: badDescriptor, Options: DefaultOptions} + directory, err := createTestDirectory() + if err != nil { + t.Fatal(err) + } + + if err = SetPolicy(directory, badPolicy); err == nil { + t.Errorf("id %q should have made SetPolicy fail", badDescriptor) + } + os.RemoveAll(directory) + } +} + +// Tests that we get back the same policy that we set on a directory +func TestGetPolicyEmptyDirectory(t *testing.T) { + directory, err := createTestDirectory() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(directory) + + var actualPolicy *PolicyData + if err = SetPolicy(directory, goodPolicy); err != nil { + t.Fatal(err) + } + if actualPolicy, err = GetPolicy(directory); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actualPolicy, goodPolicy) { + t.Errorf("policy %+v does not equal expected policy %+v", actualPolicy, goodPolicy) + } +} + +// Tests that we cannot get a policy on an unencrypted directory +func TestGetPolicyUnencrypted(t *testing.T) { + directory, err := createTestDirectory() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(directory) + + if _, err = GetPolicy(directory); err == nil { + t.Error("should have failed to set policy on a file") + } +} |