diff options
Diffstat (limited to 'filesystem')
| -rw-r--r-- | filesystem/filesystem.go | 496 | ||||
| -rw-r--r-- | filesystem/filesystem_test.go | 297 | ||||
| -rw-r--r-- | filesystem/mountpoint.go | 291 | ||||
| -rw-r--r-- | filesystem/mountpoint_test.go | 76 | ||||
| -rw-r--r-- | filesystem/path.go | 83 |
5 files changed, 1243 insertions, 0 deletions
diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go new file mode 100644 index 0000000..649345f --- /dev/null +++ b/filesystem/filesystem.go @@ -0,0 +1,496 @@ +/* + * filesystem.go - Contains the a functionality for a specific filesystem. This + * includes the commands to setup the filesystem, apply policies, and locate + * metadata. + * + * 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 filesystem deals with the structure of the files on disk used to +// store the metadata for fscrypt. Specifically, this package includes: +// - mountpoint management (mountpoint.go) +// - querying existing mounted filesystems +// - getting filesystems from a UUID +// - finding the filesystem for a specific path +// - metadata organization (filesystem.go) +// - setting up a mounted filesystem for use with fscrypt +// - adding/querying/deleting metadata +// - making links to other filesystems' metadata +// - following links to get data from other filesystems +package filesystem + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "github.com/golang/protobuf/proto" + "golang.org/x/sys/unix" + + "fscrypt/metadata" + "fscrypt/util" +) + +// FSError is the error type returned by all Mount methods. It contains an +// error value as well as the corresponding filesystem path. The error value +// is generally one of the errors defined in this package or an underlying +// error from the operating system. +type FSError struct { + Path string + Err error +} + +func (m FSError) Error() string { + return fmt.Sprintf("filesystem %q: %v", m.Path, m.Err) +} + +// Filesystem error values +var ( + ErrBadLoad = util.SystemError("couldn't load mountpoint info") + ErrRootNotMount = util.SystemError("reached root directory without finding a mountpoint") + ErrInvalidMount = errors.New("invalid mountpoint provided") + ErrNotSetup = errors.New("not setup for use with fscrypt") + ErrAlreadySetup = errors.New("already setup for use with fscrypt") + ErrBadState = util.SystemError("metadata directory in bad state: rerun setup") + ErrInvalidMetadata = errors.New("provided metadata is invalid") + ErrCorruptMetadata = util.SystemError("metadata is corrupt") + ErrNoMetadata = errors.New("no metadata could be found for the provided descriptor") + ErrLinkedProtector = errors.New("descriptor corresponds to a linked protector") + ErrCannotLink = util.SystemError("cannot create filesystem link") + ErrNoLink = util.SystemError("link does not point to a valid filesystem") + ErrOldLink = util.SystemError("link points to filesystems not using fscrypt") + ErrNoSupport = errors.New("this filesystem does not support encryption") +) + +// Mount contains information for a specific mounted filesystem. +// Path - Absolute path where the directory is mounted +// Filesystem - Name of the mounted filesystem +// Options - List of options used when mounting the filesystem +// Device - Device for filesystem (empty string if we cannot find one) +// +// In order to use a Mount to store fscrypt metadata, some directories must be +// setup first. Specifically, the directories created look like: +// <mountpoint> +// └── .fscrypt +// ├── policies +// └── protectors +// +// These "policies" and "protectors" directories will contain files that are +// the corresponding metadata structures for policies and protectors. The public +// interface includes functions for setting up these directories and Adding, +// Getting, and Removing these files. +// +// There is also the ability to reference another filesystem's metadata. This is +// used when a Policy on filesystem A is protected with Protector on filesystem +// B. In this scenario, we store a "link file" in the protectors directory whose +// contents look like "UUID=3a6d9a76-47f0-4f13-81bf-3332fbe984fb". These +// contents can be anything parsable by libblkid (i.e. anything that could be in +// the Device column of /etc/fstab). +type Mount struct { + Path string + Filesystem string + Options []string + Device string +} + +const ( + // Names of the various directories used in fscrypt + baseDirName = ".fscrypt" + policyDirName = "policies" + protectorDirName = "protectors" + tempPrefix = ".tmp" + linkFileExtension = ".link" + + // The base directory should be read-only (except for the creator) + basePermissions = 0755 + // The subdirectories should be writable to everyone, but they have the + // sticky bit set so users cannot delete other users' metadata. + dirPermissions = os.ModeSticky | 0777 + // The metadata files are globally visible, but can only be deleted by + // the user that created them + filePermissions = 0644 +) + +// baseDir returns the path of the base fscrypt directory on this filesystem. +func (m *Mount) baseDir() string { + return filepath.Join(m.Path, baseDirName) +} + +// protectorDir returns the directory containing the protector metadata. +func (m *Mount) protectorDir() string { + return filepath.Join(m.baseDir(), protectorDirName) +} + +// protectorPath returns the full path to a regular protector file with the +// specified descriptor. +func (m *Mount) protectorPath(descriptor string) string { + return filepath.Join(m.protectorDir(), descriptor) +} + +// linkedProtectorPath returns the full path to a linked protector file with the +// specified descriptor. +func (m *Mount) linkedProtectorPath(descriptor string) string { + return m.protectorPath(descriptor) + linkFileExtension +} + +// policyDir returns the directory containing the policy metadata. +func (m *Mount) policyDir() string { + return filepath.Join(m.baseDir(), policyDirName) +} + +// policyPath returns the full path to a regular policy file with the +// specified descriptor. +func (m *Mount) policyPath(descriptor string) string { + return filepath.Join(m.policyDir(), descriptor) +} + +// tempMount creates a temporary Mount under the main directory. The path for +// the returned tempMount should be removed by the caller. +func (m *Mount) tempMount() (*Mount, error) { + trashDir, err := ioutil.TempDir(m.Path, tempPrefix) + return &Mount{Path: trashDir}, err +} + +// err creates a FSErr for this filesystem with the provided error. If the +// passed error is an OS error, the full error is logged, but only the +// underlying error is used in the message. If the message is nil, nil is +// returned. +func (m *Mount) err(err error) error { + if err == nil { + return nil + } + + return FSError{ + Path: m.Path, + Err: util.UnderlyingError(err), + } +} + +// IsSetup returns true if all the fscrypt metadata directories exist. Will log +// any unexpected errors, or if any permissions are incorrect. +func (m *Mount) IsSetup() bool { + // Run all the checks so we will always get all the warnings + baseGood := isDirCheckPerm(m.baseDir(), basePermissions) + policyGood := isDirCheckPerm(m.policyDir(), dirPermissions) + protectorGood := isDirCheckPerm(m.protectorDir(), dirPermissions) + + return baseGood && policyGood && protectorGood +} + +// makeDirectories creates the three metadata directories with the correct +// permissions. Note that this function overrides the umask. +func (m *Mount) makeDirectories() error { + // Zero the umask so we get the permissions we want + oldMask := unix.Umask(0) + defer func() { + unix.Umask(oldMask) + }() + + if err := os.Mkdir(m.baseDir(), basePermissions); err != nil { + return err + } + if err := os.Mkdir(m.policyDir(), dirPermissions); err != nil { + return err + } + return os.Mkdir(m.protectorDir(), dirPermissions) +} + +// Setup sets up the filesystem for use with fscrypt, note that this merely +// creates the appropriate files on the filesystem. It does not actually modify +// the filesystem's feature flags. This operation is atomic, it either succeeds +// or no files in the baseDir are created. +func (m *Mount) Setup() error { + if m.IsSetup() { + return m.err(ErrAlreadySetup) + } + + // We build the directories under a temp Mount and then move into place. + temp, err := m.tempMount() + if err != nil { + return m.err(err) + } + defer os.RemoveAll(temp.Path) + + if err = temp.makeDirectories(); err != nil { + return m.err(err) + } + + // Move directory into place. If the base directory exists despite our + // earlier check that we were not setup, we are in bad state. + err = os.Rename(temp.baseDir(), m.baseDir()) + if os.IsExist(err) { + err = ErrBadState + } + return m.err(err) +} + +// RemoveAllMetadata removes all the policy and protector metadata from the +// filesystem. This operation is atomic, it either succeeds or no files in the +// baseDir are removed. +// WARNING: Will cause data loss if the metadata is used to encrypt +// directories (this could include directories on other filesystems). +func (m *Mount) RemoveAllMetadata() error { + if !m.IsSetup() { + return m.err(ErrNotSetup) + } + + // temp will hold the old metadata temporarily + temp, err := m.tempMount() + if err != nil { + return m.err(err) + } + defer os.RemoveAll(temp.Path) + + // Move directory into temp (to be destroyed on defer) + return m.err(os.Rename(m.baseDir(), temp.baseDir())) +} + +// writeDataAtomic writes the data to the path such that the data is either +// written to stable storage or an error is returned. +func (m *Mount) writeDataAtomic(path string, data []byte) error { + // Write the file to a temporary file then move into place so that the + // operation will be atomic. + tmpFile, err := ioutil.TempFile(filepath.Dir(path), tempPrefix) + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + + // Make sure the write actually gets to stable storage. + if _, err = tmpFile.Write(data); err != nil { + return err + } + if err = tmpFile.Sync(); err != nil { + return err + } + if err = tmpFile.Close(); err != nil { + return err + } + + return os.Rename(tmpFile.Name(), path) +} + +// addMetadata writes the metadata structure to the file with the specified +// path this will overwrite any existing data. The operation is atomic. +func (m *Mount) addMetadata(path string, md metadata.Metadata) error { + if !m.IsSetup() { + return ErrNotSetup + } + if !md.IsValid() { + return ErrInvalidMetadata + } + + data, err := proto.Marshal(md) + if err != nil { + return err + } + + log.Printf("writing metadata to %q", path) + return m.writeDataAtomic(path, data) +} + +// getMetadata reads the metadata structure from the file with the specified +// path. Only reads normal metadata files, not linked metadata. +func (m *Mount) getMetadata(path string, md metadata.Metadata) error { + if !m.IsSetup() { + return ErrNotSetup + } + + data, err := ioutil.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return ErrNoMetadata + } + return err + } + + if err = proto.Unmarshal(data, md); err != nil { + log.Print(err) + return ErrCorruptMetadata + } + + if !md.IsValid() { + log.Printf("data retrieved at %q is not valid", path) + return ErrCorruptMetadata + } + + log.Printf("successfully read metadata from %q", path) + return nil +} + +// removeMetadata deletes the metadata struct from the file with the specified +// path. Works with regular or linked metadata. +func (m *Mount) removeMetadata(path string) error { + if err := os.Remove(path); err != nil { + if os.IsNotExist(err) { + return ErrNoMetadata + } + return err + } + + log.Printf("successfully removed metadata at %q", path) + return nil +} + +// AddProtector adds the protector metadata to this filesystem's storage. This +// will overwrite the value of an existing protector with this descriptor. This +// will fail with ErrLinkedProtector if a linked protector with this descriptor +// already exists on the filesystem. +func (m *Mount) AddProtector(data *metadata.ProtectorData) error { + if isRegularFile(m.linkedProtectorPath(data.ProtectorDescriptor)) { + return m.err(ErrLinkedProtector) + } + path := m.protectorPath(data.ProtectorDescriptor) + return m.err(m.addMetadata(path, data)) +} + +// AddLinkedProtector adds a link in this filesystem to the protector metadata +// in the dest filesystem. +func (m *Mount) AddLinkedProtector(descriptor string, dest *Mount) error { + // Check that the link is good (descriptor exists, filesystem has UUID). + if _, err := dest.GetRegularProtector(descriptor); err != nil { + return err + } + + // Right now, we only make links using UUIDs. + link, err := makeLink(dest, "UUID") + if err != nil { + return dest.err(err) + } + + path := m.linkedProtectorPath(descriptor) + return m.err(m.writeDataAtomic(path, []byte(link))) +} + +// GetRegularProtector looks up the protector metadata by descriptor. This will +// fail with ErrNoMetadata if the descriptor is a linked protector. +func (m *Mount) GetRegularProtector(descriptor string) (*metadata.ProtectorData, error) { + data := new(metadata.ProtectorData) + path := m.protectorPath(descriptor) + return data, m.err(m.getMetadata(path, data)) +} + +// GetLinkedProtector returns the Mount of the filesystem containing the +// information for a linked protector and that protector's data. +func (m *Mount) GetLinkedProtector(descriptor string) (*Mount, *metadata.ProtectorData, error) { + // Get the link data from the link file + link, err := ioutil.ReadFile(m.linkedProtectorPath(descriptor)) + if err != nil { + if os.IsNotExist(err) { + err = ErrNoMetadata + } + return nil, nil, m.err(err) + } + + // As the link could refer to multiple filesystems, we check each one + // for valid metadata. + mnts, err := getMountsFromLink(string(link)) + if err != nil { + return nil, nil, m.err(err) + } + + for _, mnt := range mnts { + if data, err := mnt.GetRegularProtector(descriptor); err == nil { + return mnt, data, nil + } + } + return nil, nil, m.err(ErrOldLink) +} + +// GetEitherProtector looks up the protector metadata by descriptor. It will +// return the data for a linked protector or a regular protector. +func (m *Mount) GetEitherProtector(descriptor string) (*metadata.ProtectorData, error) { + if isRegularFile(m.linkedProtectorPath(descriptor)) { + _, data, err := m.GetLinkedProtector(descriptor) + return data, err + } + return m.GetRegularProtector(descriptor) +} + +// RemoveProtector deletes the protector metadata (or an link to another +// filesystem's metadata) from the filesystem storage. +func (m *Mount) RemoveProtector(descriptor string) error { + // We first try to remove the linkedProtector. If that metadata does not + // exist, we try to remove the normal protector. + err := m.removeMetadata(m.linkedProtectorPath(descriptor)) + if err == ErrNoMetadata { + err = m.removeMetadata(m.protectorPath(descriptor)) + } + return m.err(err) +} + +// ListProtectors lists the descriptors of all protectors on this filesystem. +// This does not include linked protectors. +func (m *Mount) ListProtectors() ([]string, error) { + protectors, err := m.listDirectory(m.protectorDir()) + return protectors, m.err(err) +} + +// AddPolicy adds the policy metadata to the filesystem storage. +func (m *Mount) AddPolicy(data *metadata.PolicyData) error { + return m.err(m.addMetadata(m.policyPath(data.KeyDescriptor), data)) +} + +// GetPolicy looks up the policy metadata by descriptor. +func (m *Mount) GetPolicy(descriptor string) (*metadata.PolicyData, error) { + data := new(metadata.PolicyData) + return data, m.err(m.getMetadata(m.policyPath(descriptor), data)) +} + +// RemovePolicy deletes the policy metadata from the filesystem storage. +func (m *Mount) RemovePolicy(descriptor string) error { + return m.err(m.removeMetadata(m.policyPath(descriptor))) +} + +// ListPolicies lists the descriptors of all policies on this filesystem. +func (m *Mount) ListPolicies() ([]string, error) { + policies, err := m.listDirectory(m.policyDir()) + return policies, m.err(err) +} + +// listDirectory returns a list of descriptors for a metadata directory, +// excluding files which are links to other filesystem's metadata. +func (m *Mount) listDirectory(directoryPath string) ([]string, error) { + if !m.IsSetup() { + return nil, ErrNotSetup + } + + log.Printf("listing descriptors in %q", directoryPath) + dir, err := os.Open(directoryPath) + if err != nil { + return nil, err + } + defer dir.Close() + + names, err := dir.Readdirnames(-1) + if err != nil { + return nil, err + } + + var descriptors []string + for _, name := range names { + if !strings.HasSuffix(name, linkFileExtension) { + descriptors = append(descriptors, name) + } + } + + log.Printf("found %d descriptor(s)", len(descriptors)) + return descriptors, nil +} diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go new file mode 100644 index 0000000..31a131a --- /dev/null +++ b/filesystem/filesystem_test.go @@ -0,0 +1,297 @@ +/* + * filesystem_test.go - Tests for reading/writing metadata to disk. + * + * 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 filesystem + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "testing" + + . "fscrypt/crypto" + . "fscrypt/metadata" +) + +var ( + fakeProtectorKey, _ = NewRandomKey(InternalKeyLen) + fakePolicyKey, _ = NewRandomKey(PolicyKeyLen) + wrappedProtectorKey, _ = Wrap(fakeProtectorKey, fakeProtectorKey) + wrappedPolicyKey, _ = Wrap(fakeProtectorKey, fakePolicyKey) +) + +// Gets the mount corresponding to TEST_FILESYSTEM_ROOT +func getTestMount() (*Mount, error) { + mountpoint := os.Getenv("TEST_FILESYSTEM_ROOT") + if mountpoint == "" { + return nil, fmt.Errorf("set TEST_FILESYSTEM_ROOT to a mountpoint") + } + mnt, err := GetMount(mountpoint) + if err != nil { + return nil, fmt.Errorf("bad TEST_FILESYSTEM_ROOT: %s", err) + } + return mnt, nil +} + +func getFakeProtector() *ProtectorData { + return &ProtectorData{ + ProtectorDescriptor: "fedcba9876543210", + Name: "goodProtector", + Source: SourceType_raw_key, + WrappedKey: wrappedProtectorKey, + } +} + +func getFakePolicy() *PolicyData { + return &PolicyData{ + KeyDescriptor: "0123456789abcdef", + Options: DefaultOptions, + WrappedPolicyKeys: []*WrappedPolicyKey{ + &WrappedPolicyKey{ + ProtectorDescriptor: "fedcba9876543210", + WrappedKey: wrappedPolicyKey, + }, + }, + } +} + +// Gets the mount and sets it up +func getSetupMount() (*Mount, error) { + mnt, err := getTestMount() + if err != nil { + return nil, err + } + return mnt, mnt.Setup() +} + +// Tests that the setup works and creates the correct files +func TestSetup(t *testing.T) { + mnt, err := getSetupMount() + if err != nil { + t.Fatal(err) + } + + if !mnt.IsSetup() { + t.Error("filesystem is not setup") + } + + os.RemoveAll(mnt.baseDir()) +} + +// Tests that we can remove all of the metadata +func TestRemoveAllMetadata(t *testing.T) { + mnt, err := getSetupMount() + if err != nil { + t.Fatal(err) + } + + if err = mnt.RemoveAllMetadata(); err != nil { + t.Fatal(err) + } + + if isDir(mnt.baseDir()) { + t.Error("metadata was not removed") + } +} + +// Adding a good Protector should succeed, adding a bad one should fail +func TestAddProtector(t *testing.T) { + mnt, err := getSetupMount() + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + + protector := getFakeProtector() + if err = mnt.AddProtector(protector); err != nil { + t.Error(err) + } + + // Change the source to bad one, or one that requires hashing costs + protector.Source = SourceType_default + if mnt.AddProtector(protector) == nil { + t.Error("bad source for a descriptor should make metadata invalid") + } + protector.Source = SourceType_custom_passphrase + if mnt.AddProtector(protector) == nil { + t.Error("protectors using passphrases should require hashing costs") + } + protector.Source = SourceType_raw_key + + // Use a bad wrapped key + protector.WrappedKey = wrappedPolicyKey + if mnt.AddProtector(protector) == nil { + t.Error("bad length for protector keys should make metadata invalid") + } + protector.WrappedKey = wrappedProtectorKey + + // Change the descriptor (to a bad length) + protector.ProtectorDescriptor = "abcde" + if mnt.AddProtector(protector) == nil { + t.Error("bad descriptor length should make metadata invalid") + } + +} + +// Adding a good Policy should succeed, adding a bad one should fail +func TestAddPolicy(t *testing.T) { + mnt, err := getSetupMount() + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + + policy := getFakePolicy() + if err = mnt.AddPolicy(policy); err != nil { + t.Error(err) + } + + // Bad encryption options should make policy invalid + policy.Options.Padding = 7 + if mnt.AddPolicy(policy) == nil { + t.Error("padding not a power of 2 should make metadata invalid") + } + policy.Options.Padding = 16 + policy.Options.Filenames = EncryptionOptions_default + if mnt.AddPolicy(policy) == nil { + t.Error("encryption mode not set should make metadata invalid") + } + policy.Options.Filenames = EncryptionOptions_CTS + + // Use a bad wrapped key + policy.WrappedPolicyKeys[0].WrappedKey = wrappedProtectorKey + if mnt.AddPolicy(policy) == nil { + t.Error("bad length for policy keys should make metadata invalid") + } + policy.WrappedPolicyKeys[0].WrappedKey = wrappedPolicyKey + + // Change the descriptor (to a bad length) + policy.KeyDescriptor = "abcde" + if mnt.AddPolicy(policy) == nil { + t.Error("bad descriptor length should make metadata invalid") + } +} + +// Tests that we can set a policy and get it back +func TestSetPolicy(t *testing.T) { + mnt, err := getSetupMount() + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + + policy := getFakePolicy() + if err = mnt.AddPolicy(policy); err != nil { + t.Fatal(err) + } + + realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(realPolicy, policy) { + t.Errorf("policy %+v does not equal expected policy %+v", realPolicy, policy) + } + +} + +// Tests that we can set a normal protector and get it back +func TestSetProtector(t *testing.T) { + mnt, err := getSetupMount() + if err != nil { + t.Fatal(err) + } + defer mnt.RemoveAllMetadata() + + protector := getFakeProtector() + if err = mnt.AddProtector(protector); err != nil { + t.Fatal(err) + } + + realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(realProtector, protector) { + t.Errorf("protector %+v does not equal expected protector %+v", realProtector, protector) + } +} + +// Gets a setup mount and a fake second mount +func getTwoSetupMounts() (realMnt, fakeMnt *Mount, err error) { + if realMnt, err = getSetupMount(); err != nil { + return + } + + // Create and setup a fake filesystem + fakeMountpoint := filepath.Join(realMnt.Path, "fake") + if err = os.MkdirAll(fakeMountpoint, basePermissions); err != nil { + return + } + fakeMnt = &Mount{Path: fakeMountpoint} + err = fakeMnt.Setup() + return +} + +// Removes all the data from the fake and real filesystems +func cleanupTwoMounts(realMnt, fakeMnt *Mount) { + realMnt.RemoveAllMetadata() + os.RemoveAll(fakeMnt.Path) +} + +// Tests that we can set a linked protector and get it back +func TestLinkedProtector(t *testing.T) { + realMnt, fakeMnt, err := getTwoSetupMounts() + if err != nil { + t.Fatal(err) + } + defer cleanupTwoMounts(realMnt, fakeMnt) + + // Add the protector to the first filesystem + protector := getFakeProtector() + if err = realMnt.AddProtector(protector); err != nil { + t.Fatal(err) + } + + // Add the link to the second filesystem + if err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt); err != nil { + t.Fatal(err) + } + + // Get the protector though the second system + _, err = fakeMnt.GetRegularProtector(protector.ProtectorDescriptor) + if err == nil || err.(FSError).Err != ErrNoMetadata { + t.Fatal(err) + } + + retMnt, retProtector, err := fakeMnt.GetLinkedProtector(protector.ProtectorDescriptor) + if err != nil { + t.Fatal(err) + } + if retMnt != realMnt { + t.Error("mount returned was incorrect") + } + + if !reflect.DeepEqual(retProtector, protector) { + t.Errorf("protector %+v does not equal expected protector %+v", retProtector, protector) + } +} diff --git a/filesystem/mountpoint.go b/filesystem/mountpoint.go new file mode 100644 index 0000000..a421058 --- /dev/null +++ b/filesystem/mountpoint.go @@ -0,0 +1,291 @@ +/* + * mountpoint.go - Contains all the functionality for finding mountpoints and + * using UUIDs to refer to them. Specifically, we can find the mountpoint of a + * path, get info about a mountpoint, and find mountpoints with a specific UUID. + * + * 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 filesystem + +/* +#cgo LDFLAGS: -lblkid -luuid +#include <blkid/blkid.h> // blkid functions +#include <stdlib.h> // free() +#include <mntent.h> // setmntent, getmntent, endmntent + +// The file containing mountpoints info and how we should read it +const char* mountpoints_filename = "/proc/mounts"; +const char* read_mode = "r"; + +// Helper function for freeing strings +void string_free(char* str) { free(str); } +*/ +import "C" + +import ( + "fmt" + "log" + "path/filepath" + "strings" + "sync" +) + +var ( + // SupportedFilesystems is a map of the filesystems which support + // filesystem-level encryption. + SupportedFilesystems = map[string]bool{ + "ext4": true, + "f2fs": true, + "ubifs": true, + } + // These maps hold data about the state of the system's mountpoints. + mountsByPath map[string]*Mount + mountsByDevice map[string][]*Mount + // Cache for information about the devices + cache C.blkid_cache + // Used to make the mount functions thread safe + mountMutex sync.Mutex + // True if the maps have been successfully initialized. + mountsInitialized bool +) + +// getMountInfo populates the Mount mappings by parsing the filesystem +// description file using the getmntent functions. Returns ErrBadLoad if the +// Mount mappings cannot be populated. +func getMountInfo() error { + if mountsInitialized { + return nil + } + + // make new maps + mountsByPath = make(map[string]*Mount) + mountsByDevice = make(map[string][]*Mount) + + // Load the mount information from mountpoints_filename + fileHandle := C.setmntent(C.mountpoints_filename, C.read_mode) + if fileHandle == nil { + return ErrBadLoad + } + defer C.endmntent(fileHandle) + + // Load the device information from the default blkid cache + if cache != nil { + C.blkid_put_cache(cache) + } + if C.blkid_get_cache(&cache, nil) != 0 { + return ErrBadLoad + } + + for { + entry := C.getmntent(fileHandle) + // When getmntent returns nil, we have read all of the entries. + if entry == nil { + mountsInitialized = true + return nil + } + + // Create the Mount structure by converting types. + mnt := Mount{ + Path: C.GoString(entry.mnt_dir), + Filesystem: C.GoString(entry.mnt_type), + Options: strings.Split(C.GoString(entry.mnt_opts), ","), + } + + // Skip invalid mountpoints + var err error + if mnt.Path, err = cannonicalizePath(mnt.Path); err != nil { + log.Print(err) + continue + } + // We can only use mountpoints that are directories for fscrypt. + if !isDir(mnt.Path) { + continue + } + + // Note this overrides the info if we have seen the mountpoint + // earlier in the file. This is correct behavior because the + // filesystems are listed in mount order. + mountsByPath[mnt.Path] = &mnt + + // Use libblkid to get the device name + cDeviceName := C.blkid_evaluate_spec(entry.mnt_fsname, &cache) + defer C.string_free(cDeviceName) + + deviceName, err := cannonicalizePath(C.GoString(cDeviceName)) + + // Only use real valid devices (unlike cgroups, tmpfs, ...) + if err == nil && isDevice(deviceName) { + mnt.Device = deviceName + mountsByDevice[deviceName] = append(mountsByDevice[deviceName], &mnt) + } + } +} + +// checkSupport returns an error if the specified mount does not support +// filesystem-level encryption. +func checkSupport(mount *Mount) error { + if SupportedFilesystems[mount.Filesystem] { + return nil + } + log.Printf("filesystem %s does not support filesystem encryption", mount.Filesystem) + return ErrNoSupport +} + +// AllSupportedFilesystems lists all the Mounts which could support filesystem +// encryption. This doesn't mean they necessarily do or that they are being used +// with fscrypt. +func AllSupportedFilesystems() (mounts []*Mount) { + mountMutex.Lock() + defer mountMutex.Unlock() + if err := getMountInfo(); err != nil { + log.Print(err) + return + } + + for _, mount := range mountsByPath { + if checkSupport(mount) == nil { + mounts = append(mounts, mount) + } + } + return +} + +// UpdateMountInfo updates the filesystem mountpoint maps with the current state +// of the filesystem mountpoints. Returns error if the initialization fails. +func UpdateMountInfo() error { + mountMutex.Lock() + defer mountMutex.Unlock() + mountsInitialized = false + return getMountInfo() +} + +// FindMount returns the corresponding Mount object for some path in a +// filesystem. Note that in the case of a bind mounts there may be two Mount +// objects for the same underlying filesystem. An error is returned if the path +// is invalid, we cannot load the required mount data, or the filesystem does +// not support filesystem encryption. If a filesystem has been updated since the +// last call to one of the mount functions, run UpdateMountInfo to see changes. +func FindMount(path string) (*Mount, error) { + path, err := cannonicalizePath(path) + if err != nil { + return nil, err + } + + mountMutex.Lock() + defer mountMutex.Unlock() + if err = getMountInfo(); err != nil { + return nil, err + } + + // Traverse up the directory tree until we find a mountpoint + for { + if mnt, ok := mountsByPath[path]; ok { + return mnt, checkSupport(mnt) + } + + // Move to the parent directory unless we have reached the root. + parent := filepath.Dir(path) + if parent == path { + return nil, ErrRootNotMount + } + path = parent + } +} + +// GetMount returns the Mount object with a matching mountpoint. An error is +// returned if the path is invalid, we cannot load the required mount data, or +// the filesystem does not support filesystem encryption. If a filesystem has +// been updated since the last call to one of the mount functions, run +// UpdateMountInfo to see changes. +func GetMount(mountpoint string) (*Mount, error) { + mountpoint, err := cannonicalizePath(mountpoint) + if err != nil { + return nil, err + } + + mountMutex.Lock() + defer mountMutex.Unlock() + if err = getMountInfo(); err != nil { + return nil, err + } + + if mnt, ok := mountsByPath[mountpoint]; ok { + return mnt, checkSupport(mnt) + } + + log.Printf("%q is not a filesystem mountpoint", mountpoint) + return nil, ErrInvalidMount +} + +// getMountsFromLink returns the Mount objects which match the provided link. +// This link can be an unparsed tag (e.g. <token>=<value>) or path (e.g. +// /dev/dm-0). The matching rules are determined by libblkid. These are the same +// matching rules for things like UUID=3a6d9a76-47f0-4f13-81bf-3332fbe984fb in +// "/etc/fstab". Note that this can match multiple Mounts. An error is returned +// if the link is invalid or we cannot load the required mount data. If a +// filesystem has been updated since the last call to one of the mount +// functions, run UpdateMountInfo to see the change. +func getMountsFromLink(link string) ([]*Mount, error) { + mountMutex.Lock() + defer mountMutex.Unlock() + if err := getMountInfo(); err != nil { + return nil, err + } + + // Use blkid to get the device + cLink := C.CString(link) + defer C.string_free(cLink) + cDeviceName := C.blkid_evaluate_spec(cLink, &cache) + defer C.string_free(cDeviceName) + + deviceName, err := cannonicalizePath(C.GoString(cDeviceName)) + if err != nil { + return nil, err + } + + if mnts, ok := mountsByDevice[deviceName]; ok { + return mnts, nil + } + + log.Printf("link %q does not refer to a device", link) + return nil, ErrNoLink +} + +// makeLink returns a link of the form <token>=<value> where value is the tag +// value for the Mount's device according to libblkid. An error is returned if +// the device/token pair has no value. +func makeLink(mnt *Mount, token string) (string, error) { + mountMutex.Lock() + defer mountMutex.Unlock() + if err := getMountInfo(); err != nil { + return "", err + } + + cToken := C.CString(token) + defer C.string_free(cToken) + cDevice := C.CString(mnt.Device) + defer C.string_free(cDevice) + + cValue := C.blkid_get_tag_value(cache, cToken, cDevice) + if cValue == nil { + log.Printf("filesystem at %q has no %s", mnt.Path, token) + return "", ErrCannotLink + } + defer C.string_free(cValue) + + return fmt.Sprintf("%s=%s", token, C.GoString(cValue)), nil +} diff --git a/filesystem/mountpoint_test.go b/filesystem/mountpoint_test.go new file mode 100644 index 0000000..2840826 --- /dev/null +++ b/filesystem/mountpoint_test.go @@ -0,0 +1,76 @@ +/* + * mountpoint_test.go - Tests for reading information about all mountpoints. + * + * 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 filesystem + +import ( + "fmt" + "testing" +) + +func printMountInfo() { + fmt.Println("\nBy Mountpoint:") + for _, mnt := range mountsByPath { + fmt.Println("\t" + mnt.Path) + fmt.Println("\t\tFilesystem: " + mnt.Filesystem) + fmt.Printf("\t\tOptions: %v\n", mnt.Options) + fmt.Println("\t\tDevice: " + mnt.Device) + } + + fmt.Println("\nBy Device:") + for device, mnts := range mountsByDevice { + fmt.Println("\t" + device) + for _, mnt := range mnts { + fmt.Println("\t\tPath: " + mnt.Path) + } + } +} + +func printSupportedMounts() { + fmt.Println("\nSupported Mountpoints:") + for _, mnt := range AllSupportedFilesystems() { + fmt.Println("\t" + mnt.Path) + fmt.Println("\t\tFilesystem: " + mnt.Filesystem) + fmt.Printf("\t\tOptions: %v\n", mnt.Options) + fmt.Println("\t\tDevice: " + mnt.Device) + } +} + +func TestLoadMountInfo(t *testing.T) { + if err := UpdateMountInfo(); err != nil { + t.Error(err) + } +} + +func TestPrintMountInfo(t *testing.T) { + // Uncomment to see the mount info in the tests + // printMountInfo() + // printSupportedMounts() + // t.Fail() +} + +// Benchmarks how long it takes to update the mountpoint data +func BenchmarkLoadFirst(b *testing.B) { + for n := 0; n < b.N; n++ { + err := UpdateMountInfo() + if err != nil { + b.Fatal(err) + } + } +} diff --git a/filesystem/path.go b/filesystem/path.go new file mode 100644 index 0000000..3be1859 --- /dev/null +++ b/filesystem/path.go @@ -0,0 +1,83 @@ +/* + * path.go - Utility functions for dealing with filesystem paths + * + * 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 filesystem + +import ( + "log" + "os" + "path/filepath" +) + +// We only check the unix permissions and the sticky bit +const permMask = os.ModeSticky | os.ModePerm + +// cannonicalizePath turns path into an absolute path without symlinks. +func cannonicalizePath(path string) (string, error) { + path, err := filepath.Abs(path) + if err != nil { + return "", err + } + + return filepath.EvalSymlinks(path) +} + +// loggedStat runs os.Stat, but it logs the error if stat returns any error +// other than nil or IsNotExist. +func loggedStat(name string) (os.FileInfo, error) { + info, err := os.Stat(name) + if err != nil && !os.IsNotExist(err) { + log.Print(err) + } + return info, err +} + +// isDir returns true if the path exists and is that of a directory. +func isDir(path string) bool { + info, err := loggedStat(path) + return err == nil && info.IsDir() +} + +// isDevice returns true if the path exists and is that of a directory. +func isDevice(path string) bool { + info, err := loggedStat(path) + return err == nil && info.Mode()&os.ModeDevice != 0 +} + +// isDirCheckPerm returns true if the path exists and is a directory. If the +// specified permissions and sticky bit of mode do not match the path, and error +// is logged. +func isDirCheckPerm(path string, mode os.FileMode) bool { + info, err := loggedStat(path) + // Check if directory + if err != nil || !info.IsDir() { + return false + } + // Check for bad permissions + if info.Mode()&permMask != mode&permMask { + log.Printf("directory %s has incorrect permissions", path) + } + return true +} + +// isRegularFile returns true if the path exists and is that of a regular file. +func isRegularFile(path string) bool { + info, err := loggedStat(path) + return err == nil && info.Mode().IsRegular() +} |