aboutsummaryrefslogtreecommitdiff
path: root/filesystem/filesystem_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'filesystem/filesystem_test.go')
-rw-r--r--filesystem/filesystem_test.go371
1 files changed, 343 insertions, 28 deletions
diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go
index 04d5123..f9c34ae 100644
--- a/filesystem/filesystem_test.go
+++ b/filesystem/filesystem_test.go
@@ -21,11 +21,13 @@ package filesystem
import (
"os"
+ "os/user"
"path/filepath"
- "reflect"
+ "syscall"
"testing"
- "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+ "google.golang.org/protobuf/proto"
"github.com/google/fscrypt/crypto"
"github.com/google/fscrypt/metadata"
@@ -57,6 +59,19 @@ func getFakeProtector() *metadata.ProtectorData {
}
}
+func getFakeLoginProtector(uid int64) *metadata.ProtectorData {
+ protector := getFakeProtector()
+ protector.Source = metadata.SourceType_pam_passphrase
+ protector.Uid = uid
+ protector.Costs = &metadata.HashingCosts{
+ Time: 1,
+ Memory: 1 << 8,
+ Parallelism: 1,
+ }
+ protector.Salt = make([]byte, 16)
+ return protector
+}
+
func getFakePolicy() *metadata.PolicyData {
return &metadata.PolicyData{
KeyDescriptor: "0123456789abcdef",
@@ -76,7 +91,7 @@ func getSetupMount(t *testing.T) (*Mount, error) {
if err != nil {
return nil, err
}
- return mnt, mnt.Setup()
+ return mnt, mnt.Setup(WorldWritable)
}
// Tests that the setup works and creates the correct files
@@ -86,7 +101,7 @@ func TestSetup(t *testing.T) {
t.Fatal(err)
}
- if err := mnt.CheckSetup(); err != nil {
+ if err := mnt.CheckSetup(nil); err != nil {
t.Error(err)
}
@@ -109,6 +124,125 @@ func TestRemoveAllMetadata(t *testing.T) {
}
}
+// isSymlink returns true if the path exists and is that of a symlink.
+func isSymlink(path string) bool {
+ info, err := loggedLstat(path)
+ return err == nil && info.Mode()&os.ModeSymlink != 0
+}
+
+// Test that when MOUNTPOINT/.fscrypt is a pre-created symlink, fscrypt will
+// create/delete the metadata at the location pointed to by the symlink.
+//
+// This is a helper function that is called twice: once to test an absolute
+// symlink and once to test a relative symlink.
+func testSetupWithSymlink(t *testing.T, mnt *Mount, symlinkTarget string, realDir string) {
+ rawBaseDir := filepath.Join(mnt.Path, baseDirName)
+ if err := os.Symlink(symlinkTarget, rawBaseDir); err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(rawBaseDir)
+
+ if err := mnt.Setup(WorldWritable); err != nil {
+ t.Fatal(err)
+ }
+ defer mnt.RemoveAllMetadata()
+ if err := mnt.CheckSetup(nil); err != nil {
+ t.Fatal(err)
+ }
+ if !isSymlink(rawBaseDir) {
+ t.Fatal("base dir should still be a symlink")
+ }
+ if !isDir(realDir) {
+ t.Fatal("real base dir should exist")
+ }
+ if err := mnt.RemoveAllMetadata(); err != nil {
+ t.Fatal(err)
+ }
+ if !isSymlink(rawBaseDir) {
+ t.Fatal("base dir should still be a symlink")
+ }
+ if isDir(realDir) {
+ t.Fatal("real base dir should no longer exist")
+ }
+}
+
+func TestSetupWithAbsoluteSymlink(t *testing.T) {
+ mnt, err := getTestMount(t)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tempDir, err := os.MkdirTemp("", "fscrypt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+ realDir := filepath.Join(tempDir, "realDir")
+ if realDir, err = filepath.Abs(realDir); err != nil {
+ t.Fatal(err)
+ }
+ testSetupWithSymlink(t, mnt, realDir, realDir)
+}
+
+func TestSetupWithRelativeSymlink(t *testing.T) {
+ mnt, err := getTestMount(t)
+ if err != nil {
+ t.Fatal(err)
+ }
+ realDir := filepath.Join(mnt.Path, ".fscrypt-real")
+ testSetupWithSymlink(t, mnt, ".fscrypt-real", realDir)
+}
+
+func testSetupMode(t *testing.T, mnt *Mount, setupMode SetupMode, expectedPerms os.FileMode) {
+ mnt.RemoveAllMetadata()
+ if err := mnt.Setup(setupMode); err != nil {
+ t.Fatal(err)
+ }
+ dirNames := []string{"policies", "protectors"}
+ for _, dirName := range dirNames {
+ fi, err := os.Stat(filepath.Join(mnt.Path, ".fscrypt", dirName))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fi.Mode()&(os.ModeSticky|0777) != expectedPerms {
+ t.Errorf("directory %s doesn't have permissions %o", dirName, expectedPerms)
+ }
+ }
+}
+
+// Tests that the supported setup modes (WorldWritable and SingleUserWritable)
+// work as intended.
+func TestSetupModes(t *testing.T) {
+ mnt, err := getTestMount(t)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer mnt.RemoveAllMetadata()
+ testSetupMode(t, mnt, WorldWritable, os.ModeSticky|0777)
+ testSetupMode(t, mnt, SingleUserWritable, 0755)
+}
+
+// Tests that fscrypt refuses to use metadata directories that are
+// world-writable but don't have the sticky bit set.
+func TestInsecurePermissions(t *testing.T) {
+ mnt, err := getTestMount(t)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer mnt.RemoveAllMetadata()
+
+ if err = mnt.Setup(WorldWritable); err != nil {
+ t.Fatal(err)
+ }
+ if err = os.Chmod(mnt.PolicyDir(), 0777); err != nil {
+ t.Fatal(err)
+ }
+ defer os.Chmod(mnt.PolicyDir(), os.ModeSticky|0777)
+ err = mnt.CheckSetup(nil)
+ if _, ok := err.(*ErrInsecurePermissions); !ok {
+ t.Fatal("expected ErrInsecurePermissions")
+ }
+}
+
// Adding a good Protector should succeed, adding a bad one should fail
func TestAddProtector(t *testing.T) {
mnt, err := getSetupMount(t)
@@ -118,31 +252,31 @@ func TestAddProtector(t *testing.T) {
defer mnt.RemoveAllMetadata()
protector := getFakeProtector()
- if err = mnt.AddProtector(protector); err != nil {
+ if err = mnt.AddProtector(protector, nil); err != nil {
t.Error(err)
}
// Change the source to bad one, or one that requires hashing costs
protector.Source = metadata.SourceType_default
- if mnt.AddProtector(protector) == nil {
+ if mnt.AddProtector(protector, nil) == nil {
t.Error("bad source for a descriptor should make metadata invalid")
}
protector.Source = metadata.SourceType_custom_passphrase
- if mnt.AddProtector(protector) == nil {
+ if mnt.AddProtector(protector, nil) == nil {
t.Error("protectors using passphrases should require hashing costs")
}
protector.Source = metadata.SourceType_raw_key
// Use a bad wrapped key
protector.WrappedKey = wrappedPolicyKey
- if mnt.AddProtector(protector) == nil {
+ if mnt.AddProtector(protector, nil) == 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 {
+ if mnt.AddProtector(protector, nil) == nil {
t.Error("bad descriptor length should make metadata invalid")
}
@@ -157,32 +291,32 @@ func TestAddPolicy(t *testing.T) {
defer mnt.RemoveAllMetadata()
policy := getFakePolicy()
- if err = mnt.AddPolicy(policy); err != nil {
+ if err = mnt.AddPolicy(policy, nil); err != nil {
t.Error(err)
}
// Bad encryption options should make policy invalid
policy.Options.Padding = 7
- if mnt.AddPolicy(policy) == nil {
+ if mnt.AddPolicy(policy, nil) == nil {
t.Error("padding not a power of 2 should make metadata invalid")
}
policy.Options.Padding = 16
policy.Options.Filenames = metadata.EncryptionOptions_default
- if mnt.AddPolicy(policy) == nil {
+ if mnt.AddPolicy(policy, nil) == nil {
t.Error("encryption mode not set should make metadata invalid")
}
policy.Options.Filenames = metadata.EncryptionOptions_AES_256_CTS
// Use a bad wrapped key
policy.WrappedPolicyKeys[0].WrappedKey = wrappedProtectorKey
- if mnt.AddPolicy(policy) == nil {
+ if mnt.AddPolicy(policy, nil) == 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 {
+ if mnt.AddPolicy(policy, nil) == nil {
t.Error("bad descriptor length should make metadata invalid")
}
}
@@ -196,16 +330,16 @@ func TestSetPolicy(t *testing.T) {
defer mnt.RemoveAllMetadata()
policy := getFakePolicy()
- if err = mnt.AddPolicy(policy); err != nil {
+ if err = mnt.AddPolicy(policy, nil); err != nil {
t.Fatal(err)
}
- realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor)
+ realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor, nil)
if err != nil {
t.Fatal(err)
}
- if !reflect.DeepEqual(realPolicy, policy) {
+ if !proto.Equal(realPolicy, policy) {
t.Errorf("policy %+v does not equal expected policy %+v", realPolicy, policy)
}
@@ -220,20 +354,99 @@ func TestSetProtector(t *testing.T) {
defer mnt.RemoveAllMetadata()
protector := getFakeProtector()
- if err = mnt.AddProtector(protector); err != nil {
+ if err = mnt.AddProtector(protector, nil); err != nil {
t.Fatal(err)
}
- realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor)
+ realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
if err != nil {
t.Fatal(err)
}
- if !reflect.DeepEqual(realProtector, protector) {
+ if !proto.Equal(realProtector, protector) {
t.Errorf("protector %+v does not equal expected protector %+v", realProtector, protector)
}
}
+// Tests that a login protector whose embedded UID doesn't match the file owner
+// is considered invalid. (Such a file could be created by a malicious user to
+// try to confuse fscrypt into processing the wrong file.)
+func TestSpoofedLoginProtector(t *testing.T) {
+ myUID := int64(os.Geteuid())
+ badUID := myUID + 1 // anything different from myUID
+ mnt, err := getSetupMount(t)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer mnt.RemoveAllMetadata()
+
+ // Control case: protector with matching UID should be accepted.
+ protector := getFakeLoginProtector(myUID)
+ if err = mnt.AddProtector(protector, nil); err != nil {
+ t.Fatal(err)
+ }
+ _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = mnt.RemoveProtector(protector.ProtectorDescriptor); err != nil {
+ t.Fatal(err)
+ }
+
+ // The real test: protector with mismatching UID should rejected,
+ // *unless* the process running the tests (and hence the file owner) is
+ // root in which case it should be accepted.
+ protector = getFakeLoginProtector(badUID)
+ if err = mnt.AddProtector(protector, nil); err != nil {
+ t.Fatal(err)
+ }
+ _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
+ if myUID == 0 {
+ if err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ if err == nil {
+ t.Fatal("reading protector with bad UID unexpectedly succeeded")
+ }
+ }
+}
+
+// Tests that the fscrypt metadata files are given mode 0600.
+func TestMetadataFileMode(t *testing.T) {
+ mnt, err := getSetupMount(t)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer mnt.RemoveAllMetadata()
+
+ // Policy
+ policy := getFakePolicy()
+ if err = mnt.AddPolicy(policy, nil); err != nil {
+ t.Fatal(err)
+ }
+ fi, err := os.Stat(filepath.Join(mnt.Path, ".fscrypt/policies/", policy.KeyDescriptor))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fi.Mode()&0777 != 0600 {
+ t.Error("Policy file has wrong mode")
+ }
+
+ // Protector
+ protector := getFakeProtector()
+ if err = mnt.AddProtector(protector, nil); err != nil {
+ t.Fatal(err)
+ }
+ fi, err = os.Stat(filepath.Join(mnt.Path, ".fscrypt/protectors", protector.ProtectorDescriptor))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fi.Mode()&0777 != 0600 {
+ t.Error("Protector file has wrong mode")
+ }
+}
+
// Gets a setup mount and a fake second mount
func getTwoSetupMounts(t *testing.T) (realMnt, fakeMnt *Mount, err error) {
if realMnt, err = getSetupMount(t); err != nil {
@@ -245,8 +458,8 @@ func getTwoSetupMounts(t *testing.T) (realMnt, fakeMnt *Mount, err error) {
if err = os.MkdirAll(fakeMountpoint, basePermissions); err != nil {
return
}
- fakeMnt = &Mount{Path: fakeMountpoint}
- err = fakeMnt.Setup()
+ fakeMnt = &Mount{Path: fakeMountpoint, FilesystemType: realMnt.FilesystemType}
+ err = fakeMnt.Setup(WorldWritable)
return
}
@@ -266,22 +479,32 @@ func TestLinkedProtector(t *testing.T) {
// Add the protector to the first filesystem
protector := getFakeProtector()
- if err = realMnt.AddProtector(protector); err != nil {
+ if err = realMnt.AddProtector(protector, nil); err != nil {
t.Fatal(err)
}
// Add the link to the second filesystem
- if err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt); err != nil {
+ var isNewLink bool
+ if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil, nil); err != nil {
t.Fatal(err)
}
+ if !isNewLink {
+ t.Fatal("Link was not new")
+ }
+ if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ if isNewLink {
+ t.Fatal("Link was new")
+ }
// Get the protector though the second system
- _, err = fakeMnt.GetRegularProtector(protector.ProtectorDescriptor)
- if errors.Cause(err) != ErrNoMetadata {
+ _, err = fakeMnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
+ if _, ok := err.(*ErrProtectorNotFound); !ok {
t.Fatal(err)
}
- retMnt, retProtector, err := fakeMnt.GetProtector(protector.ProtectorDescriptor)
+ retMnt, retProtector, err := fakeMnt.GetProtector(protector.ProtectorDescriptor, nil)
if err != nil {
t.Fatal(err)
}
@@ -289,7 +512,99 @@ func TestLinkedProtector(t *testing.T) {
t.Error("mount returned was incorrect")
}
- if !reflect.DeepEqual(retProtector, protector) {
+ if !proto.Equal(retProtector, protector) {
t.Errorf("protector %+v does not equal expected protector %+v", retProtector, protector)
}
}
+
+func createFile(path string, size int64) error {
+ if err := os.WriteFile(path, []byte{}, 0600); err != nil {
+ return err
+ }
+ return os.Truncate(path, size)
+}
+
+// Tests the readMetadataFileSafe() function.
+func TestReadMetadataFileSafe(t *testing.T) {
+ currentUser, err := util.EffectiveUser()
+ otherUser := &user.User{Uid: "-1"}
+ if err != nil {
+ t.Fatal(err)
+ }
+ tempDir, err := os.MkdirTemp("", "fscrypt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ filePath := filepath.Join(tempDir, "file")
+ defer os.RemoveAll(tempDir)
+
+ // Good file (control case)
+ if err = createFile(filePath, 1000); err != nil {
+ t.Fatal(err)
+ }
+ _, owner, err := readMetadataFileSafe(filePath, nil)
+ if err != nil {
+ t.Fatal("failed to read file")
+ }
+ if owner != int64(os.Geteuid()) {
+ t.Fatal("got wrong owner")
+ }
+ // Also try it with the trustedUser argument set to the current user.
+ if _, _, err = readMetadataFileSafe(filePath, currentUser); err != nil {
+ t.Fatal("failed to read file")
+ }
+ os.Remove(filePath)
+
+ // File owned by another user. We might not have permission to actually
+ // change the file's ownership, so we simulate this by passing in a bad
+ // value for the trustedUser argument.
+ if err = createFile(filePath, 1000); err != nil {
+ t.Fatal(err)
+ }
+ _, _, err = readMetadataFileSafe(filePath, otherUser)
+ if util.IsUserRoot() {
+ if err != nil {
+ t.Fatal("root-owned file didn't pass owner validation")
+ }
+ } else {
+ if err == nil {
+ t.Fatal("unexpectedly could read file owned by another user")
+ }
+ }
+ os.Remove(filePath)
+
+ // Nonexistent file
+ _, _, err = readMetadataFileSafe(filePath, nil)
+ if !os.IsNotExist(err) {
+ t.Fatal("trying to read nonexistent file didn't fail with expected error")
+ }
+
+ // Symlink
+ if err = os.Symlink("target", filePath); err != nil {
+ t.Fatal(err)
+ }
+ _, _, err = readMetadataFileSafe(filePath, nil)
+ if err.(*os.PathError).Err != syscall.ELOOP {
+ t.Fatal("trying to read symlink didn't fail with ELOOP")
+ }
+ os.Remove(filePath)
+
+ // FIFO
+ if err = unix.Mkfifo(filePath, 0600); err != nil {
+ t.Fatal(err)
+ }
+ _, _, err = readMetadataFileSafe(filePath, nil)
+ if _, ok := err.(*ErrCorruptMetadata); !ok {
+ t.Fatal("trying to read FIFO didn't fail with expected error")
+ }
+ os.Remove(filePath)
+
+ // Very large file
+ if err = createFile(filePath, 1000000); err != nil {
+ t.Fatal(err)
+ }
+ _, _, err = readMetadataFileSafe(filePath, nil)
+ if _, ok := err.(*ErrCorruptMetadata); !ok {
+ t.Fatal("trying to read very large file didn't fail with expected error")
+ }
+}