--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+ }
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+ }
+ }
+}
--- /dev/null
+/*
+ * 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()
+}