aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Richey <joerichey@google.com>2017-03-02 10:38:33 -0800
committerJoe Richey joerichey@google.com <joerichey@google.com>2017-05-02 13:39:18 -0700
commita683ab55245aa44ada5059f8e9816adbd94198ff (patch)
tree13b966a777803e1b2a870b89219c3440259e9ffb
parent06e86bde8886aca3e1cd8cbc948cff7a103f97ab (diff)
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
-rw-r--r--Makefile3
-rw-r--r--README.md13
-rw-r--r--metadata/config.go6
-rw-r--r--metadata/config_test.go6
-rw-r--r--metadata/metadata.pb.go2
-rw-r--r--metadata/metadata.proto2
-rw-r--r--metadata/policy.go166
-rw-r--r--metadata/policy_test.go162
-rw-r--r--util/errors.go15
-rw-r--r--util/util.go22
10 files changed, 382 insertions, 15 deletions
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
+}