From a683ab55245aa44ada5059f8e9816adbd94198ff Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Thu, 2 Mar 2017 10:38:33 -0800 Subject: [PATCH] metadata: get and set policies from go This commit adds in the ability to get and set policy data from go using the GetPolicy and SetPolicy functions. This is done via a patch of the x/sys/unix package that exposes the filesystem encryption structures. Note that not all the fields of the PolicyData protocol buffer are needed to get and set policies. The wrapped_policy_keys are not used and will be written and read by other components of fscrypt. To run the policy tests, the environment variable BASE_TEST_DIR must be set to a directory for testing on a filesystem that supports encryption. Change-Id: I13b1d983356845f3ffc1945cedf53234218f32e5 --- Makefile | 3 +- README.md | 13 ++-- metadata/config.go | 6 +- metadata/config_test.go | 6 +- metadata/metadata.pb.go | 2 +- metadata/metadata.proto | 2 +- metadata/policy.go | 166 ++++++++++++++++++++++++++++++++++++++++ metadata/policy_test.go | 162 +++++++++++++++++++++++++++++++++++++++ util/errors.go | 15 ++++ util/util.go | 22 ++++++ 10 files changed, 382 insertions(+), 15 deletions(-) create mode 100644 metadata/policy.go create mode 100644 metadata/policy_test.go diff --git a/Makefile b/Makefile index 0c38d91..ac41537 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,8 @@ go: govendor test $(GOFLAGS) +local update: - @govendor fetch +external +vendor +missing + @govendor fetch +missing + @govendor add +external @govendor remove +unused lint: diff --git a/README.md b/README.md index ba31826..e9cb5c8 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ You will also want to add `$GOPATH/bin` to your `$PATH`. `fscrypt` has the following build dependencies: * `make` -* A C compiler ('gcc' or 'clang') +* A C compiler (`gcc` or `clang`) * Go Once this is setup, you can run `make fscrypt` to build the executable in @@ -81,10 +81,13 @@ dynamically linked binary by default. ## Running and Installing -`fscrypt` currently has no runtime dependencies. Installing it just requires -placing it in your path or running `make install`. Change `$GOBIN` to change the -install location of `fscrypt`. By default, `fscrypt` is installed to -`$GOPATH/bin`. +`fscrypt` has the following runtime dependencies: +* Kernel support for filesystem encryption (this will depend on your kernel + configuration and specific filesystem) + +Installing it just requires placing it in your path or running `make install`. +Change `$GOBIN` to change the install location of `fscrypt`. By default, +`fscrypt` is installed to `$GOPATH/bin`. ## Example Usage 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") + } +} diff --git a/util/errors.go b/util/errors.go index aafeadd..bd63ac8 100644 --- a/util/errors.go +++ b/util/errors.go @@ -22,6 +22,7 @@ package util import ( "fmt" "log" + "os" ) // InvalidInputF creates an error that should indicate either bad input from a @@ -48,3 +49,17 @@ func NeverError(err error) { log.Panicf("NeverError() check failed: %v", err) } } + +// UnderlyingError returns the underlying error for known os error types. +// From: src/os/error.go +func UnderlyingError(err error) error { + switch err := err.(type) { + case *os.PathError: + return err.Err + case *os.LinkError: + return err.Err + case *os.SyscallError: + return err.Err + } + return err +} diff --git a/util/util.go b/util/util.go index 9439d0e..7574f35 100644 --- a/util/util.go +++ b/util/util.go @@ -31,3 +31,25 @@ import ( func Ptr(slice []byte) unsafe.Pointer { return unsafe.Pointer(&slice[0]) } + +// Index returns the first index i such that inVal == inArray[i]. +// ok is true if we find a match, false otherwise. +func Index(inVal int64, inArray []int64) (index int, ok bool) { + for index, val := range inArray { + if val == inVal { + return index, true + } + } + return 0, false +} + +// Lookup finds inVal in inArray and returns the corresponding element in +// outArray. Specifically, if inVal == inArray[i], outVal == outArray[i]. +// ok is true if we find a match, false otherwise. +func Lookup(inVal int64, inArray, outArray []int64) (outVal int64, ok bool) { + index, ok := Index(inVal, inArray) + if !ok { + return 0, false + } + return outArray[index], true +} -- 2.39.5