From 1a47718420317f893831b0223153d56005d5b02b Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: filesystem: validate size and type of metadata files Don't allow reading metadata files that are very large, as they can crash the program due to the memory required. Similarly, don't allow reading metadata files that aren't regular files, such as FIFOs, or symlinks (which could point to a device node like /dev/zero), as that can hang the program. Both issues were particularly problematic for pam_fscrypt, as they could prevent users from being able to log in. Note: these checks are arguably unneeded if we strictly check the file ownership too, which a later commit will do. But there's no reason not to do these basic checks too. --- filesystem/filesystem_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) (limited to 'filesystem/filesystem_test.go') diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index 92726b2..71724ae 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -24,9 +24,11 @@ import ( "log" "os" "path/filepath" + "syscall" "testing" "github.com/golang/protobuf/proto" + "golang.org/x/sys/unix" "github.com/google/fscrypt/crypto" "github.com/google/fscrypt/metadata" @@ -382,3 +384,64 @@ func TestLinkedProtector(t *testing.T) { t.Errorf("protector %+v does not equal expected protector %+v", retProtector, protector) } } + +func createFile(path string, size int64) error { + if err := ioutil.WriteFile(path, []byte{}, 0600); err != nil { + return err + } + return os.Truncate(path, size) +} + +// Tests the readMetadataFileSafe() function. +func TestReadMetadataFileSafe(t *testing.T) { + tempDir, err := ioutil.TempDir("", "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) + } + if _, err = readMetadataFileSafe(filePath); err != nil { + t.Fatal("failed to read file") + } + os.Remove(filePath) + + // Nonexistent file + _, err = readMetadataFileSafe(filePath) + 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) + 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) + 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) + if _, ok := err.(*ErrCorruptMetadata); !ok { + t.Fatal("trying to read very large file didn't fail with expected error") + } +} -- cgit v1.2.3 From b44fbe71e1e93c47050322af51725bac997641e0 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: filesystem: reject spoofed login protectors If a login protector contains a UID that differs from the file owner (and the file owner is not root), it might be a spoofed file that was created maliciously, so make sure to consider such files to be invalid. --- filesystem/filesystem_test.go | 71 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) (limited to 'filesystem/filesystem_test.go') diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index 71724ae..7aa97cb 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -60,6 +60,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", @@ -315,6 +328,50 @@ func TestSetProtector(t *testing.T) { } } +// 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); err != nil { + t.Fatal(err) + } + _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor) + 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); err != nil { + t.Fatal(err) + } + _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor) + if myUID == 0 { + if err != nil { + t.Fatal(err) + } + } else { + if err == nil { + t.Fatal("reading protector with bad UID unexpectedly succeeded") + } + } +} + // 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 { @@ -405,13 +462,17 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = createFile(filePath, 1000); err != nil { t.Fatal(err) } - if _, err = readMetadataFileSafe(filePath); err != nil { + _, owner, err := readMetadataFileSafe(filePath) + if err != nil { t.Fatal("failed to read file") } + if owner != int64(os.Geteuid()) { + t.Fatal("got wrong owner") + } os.Remove(filePath) // Nonexistent file - _, err = readMetadataFileSafe(filePath) + _, _, err = readMetadataFileSafe(filePath) if !os.IsNotExist(err) { t.Fatal("trying to read nonexistent file didn't fail with expected error") } @@ -420,7 +481,7 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = os.Symlink("target", filePath); err != nil { t.Fatal(err) } - _, err = readMetadataFileSafe(filePath) + _, _, err = readMetadataFileSafe(filePath) if err.(*os.PathError).Err != syscall.ELOOP { t.Fatal("trying to read symlink didn't fail with ELOOP") } @@ -430,7 +491,7 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = unix.Mkfifo(filePath, 0600); err != nil { t.Fatal(err) } - _, err = readMetadataFileSafe(filePath) + _, _, err = readMetadataFileSafe(filePath) if _, ok := err.(*ErrCorruptMetadata); !ok { t.Fatal("trying to read FIFO didn't fail with expected error") } @@ -440,7 +501,7 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = createFile(filePath, 1000000); err != nil { t.Fatal(err) } - _, err = readMetadataFileSafe(filePath) + _, _, err = readMetadataFileSafe(filePath) if _, ok := err.(*ErrCorruptMetadata); !ok { t.Fatal("trying to read very large file didn't fail with expected error") } -- cgit v1.2.3 From 6e355131670ad014e45f879475ddf800f0080d41 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Make 'fscrypt setup' offer a choice of directory modes World-writable directories are not appropriate for some systems, so offer a choice of single-user-writable and world-writable modes, with single-user-writable being the default. Add a new documentation section to help users decide which one to use. --- filesystem/filesystem_test.go | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) (limited to 'filesystem/filesystem_test.go') diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index 7aa97cb..365c5cb 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -92,7 +92,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 @@ -153,7 +153,7 @@ func testSetupWithSymlink(t *testing.T, mnt *Mount, symlinkTarget string, realDi } defer os.Remove(rawBaseDir) - if err := mnt.Setup(); err != nil { + if err := mnt.Setup(WorldWritable); err != nil { t.Fatal(err) } defer mnt.RemoveAllMetadata() @@ -203,6 +203,35 @@ func TestSetupWithRelativeSymlink(t *testing.T) { 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) +} + // Adding a good Protector should succeed, adding a bad one should fail func TestAddProtector(t *testing.T) { mnt, err := getSetupMount(t) @@ -384,7 +413,7 @@ func getTwoSetupMounts(t *testing.T) (realMnt, fakeMnt *Mount, err error) { return } fakeMnt = &Mount{Path: fakeMountpoint, FilesystemType: realMnt.FilesystemType} - err = fakeMnt.Setup() + err = fakeMnt.Setup(WorldWritable) return } -- cgit v1.2.3 From 74e870b7bd1585b4b509da47e0e75db66336e576 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Strictly validate metadata file ownership by default The metadata validation checks introduced by the previous commits are good, but to reduce the attack surface it would be much better to avoid reading and parsing files owned by other users in the first place. There are some possible use cases for users sharing fscrypt metadata files, but I think that for the vast majority of users it is unneeded and just opens up attack surface. Thus, make fscrypt (and pam_fscrypt) not process policies or protectors owned by other users by default. Specifically, * If fscrypt or pam_fscrypt is running as a non-root user, only policies and protectors owned by the user or by root can be used. * If fscrypt is running as root, any policy or protector can be used. (This is to match user expectations -- starting a sudo session should gain rights, not remove rights.) * If pam_fscrypt is running as root, only policies and protectors owned by root can be used. Note that this only applies when the root user themselves has an fscrypt login protector, which is rare. Add an option 'allow_cross_user_metadata' to /etc/fscrypt.conf which allows restoring the old behavior for anyone who really needs it. --- filesystem/filesystem_test.go | 54 ++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 13 deletions(-) (limited to 'filesystem/filesystem_test.go') diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index 365c5cb..d4ef826 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -23,6 +23,7 @@ import ( "io/ioutil" "log" "os" + "os/user" "path/filepath" "syscall" "testing" @@ -323,7 +324,7 @@ func TestSetPolicy(t *testing.T) { t.Fatal(err) } - realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor) + realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor, nil) if err != nil { t.Fatal(err) } @@ -347,7 +348,7 @@ func TestSetProtector(t *testing.T) { t.Fatal(err) } - realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor) + realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) if err != nil { t.Fatal(err) } @@ -374,7 +375,7 @@ func TestSpoofedLoginProtector(t *testing.T) { if err = mnt.AddProtector(protector); err != nil { t.Fatal(err) } - _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor) + _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) if err != nil { t.Fatal(err) } @@ -389,7 +390,7 @@ func TestSpoofedLoginProtector(t *testing.T) { if err = mnt.AddProtector(protector); err != nil { t.Fatal(err) } - _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor) + _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) if myUID == 0 { if err != nil { t.Fatal(err) @@ -439,13 +440,13 @@ func TestLinkedProtector(t *testing.T) { // Add the link to the second filesystem var isNewLink bool - if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt); err != nil { + if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil); err != nil { t.Fatal(err) } if !isNewLink { t.Fatal("Link was not new") } - if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt); err != nil { + if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil); err != nil { t.Fatal(err) } if isNewLink { @@ -453,12 +454,12 @@ func TestLinkedProtector(t *testing.T) { } // Get the protector though the second system - _, err = fakeMnt.GetRegularProtector(protector.ProtectorDescriptor) + _, 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) } @@ -480,6 +481,11 @@ func createFile(path string, size int64) error { // 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 := ioutil.TempDir("", "fscrypt") if err != nil { t.Fatal(err) @@ -491,17 +497,39 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = createFile(filePath, 1000); err != nil { t.Fatal(err) } - _, owner, err := readMetadataFileSafe(filePath) + _, 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) + _, _, err = readMetadataFileSafe(filePath, nil) if !os.IsNotExist(err) { t.Fatal("trying to read nonexistent file didn't fail with expected error") } @@ -510,7 +538,7 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = os.Symlink("target", filePath); err != nil { t.Fatal(err) } - _, _, err = readMetadataFileSafe(filePath) + _, _, err = readMetadataFileSafe(filePath, nil) if err.(*os.PathError).Err != syscall.ELOOP { t.Fatal("trying to read symlink didn't fail with ELOOP") } @@ -520,7 +548,7 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = unix.Mkfifo(filePath, 0600); err != nil { t.Fatal(err) } - _, _, err = readMetadataFileSafe(filePath) + _, _, err = readMetadataFileSafe(filePath, nil) if _, ok := err.(*ErrCorruptMetadata); !ok { t.Fatal("trying to read FIFO didn't fail with expected error") } @@ -530,7 +558,7 @@ func TestReadMetadataFileSafe(t *testing.T) { if err = createFile(filePath, 1000000); err != nil { t.Fatal(err) } - _, _, err = readMetadataFileSafe(filePath) + _, _, err = readMetadataFileSafe(filePath, nil) if _, ok := err.(*ErrCorruptMetadata); !ok { t.Fatal("trying to read very large file didn't fail with expected error") } -- cgit v1.2.3 From 85a747493ff368a72f511619ecd391016ecb933c Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Extend ownership validation to entire directory structure A previous commit extended file ownership validation to policy and protector files (by default -- there's an opt-out in /etc/fscrypt.conf). However, that didn't apply to the parent directories: MOUNTPOINT MOUNTPOINT/.fscrypt MOUNTPOINT/.fscrypt/policies MOUNTPOINT/.fscrypt/protectors The problem is that if the parent directories aren't trusted (owned by another non-root user), then untrusted changes to their contents can be made at any time, including the introduction of symlinks and so on. While it's debatable how much of a problem this really is, given the other validations that are done, it seems to be appropriate to validate the parent directories too. Therefore, this commit applies the same ownership validations to the above four directories as are done on the metadata files themselves. In addition, it is validated that none of these directories are symlinks except for ".fscrypt" where this is explicitly supported. --- filesystem/filesystem_test.go | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) (limited to 'filesystem/filesystem_test.go') diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index d4ef826..92e113b 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -21,7 +21,6 @@ package filesystem import ( "io/ioutil" - "log" "os" "os/user" "path/filepath" @@ -103,7 +102,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) } @@ -126,16 +125,6 @@ func TestRemoveAllMetadata(t *testing.T) { } } -// loggedLstat runs os.Lstat (doesn't dereference trailing symlink), but it logs -// the error if lstat returns any error other than nil or IsNotExist. -func loggedLstat(name string) (os.FileInfo, error) { - info, err := os.Lstat(name) - if err != nil && !os.IsNotExist(err) { - log.Print(err) - } - return info, err -} - // isSymlink returns true if the path exists and is that of a symlink. func isSymlink(path string) bool { info, err := loggedLstat(path) @@ -158,7 +147,7 @@ func testSetupWithSymlink(t *testing.T, mnt *Mount, symlinkTarget string, realDi t.Fatal(err) } defer mnt.RemoveAllMetadata() - if err := mnt.CheckSetup(); err != nil { + if err := mnt.CheckSetup(nil); err != nil { t.Fatal(err) } if !isSymlink(rawBaseDir) { @@ -233,6 +222,28 @@ func TestSetupModes(t *testing.T) { 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) -- cgit v1.2.3 From d4ce0b892cbe68db9f90f4015342e6a9069b079c Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Make all new metadata files owned by user when needed Since commit 4c7c6631cc5a ("Set owner of login protectors to correct user"), login protectors are made owned by the user when root creates one on a user's behalf. That's good, but the same isn't true of other files that get created at the same time: - The policy protecting the directory - The protector link file, if the policy is on a different filesystem - The recovery protector, if the policy is on a different filesystem - The recovery instructions file In preparation for setting all metadata files to mode 0600, start making all these files owned by the user in this scenario as well. --- filesystem/filesystem_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) (limited to 'filesystem/filesystem_test.go') diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index 92e113b..f74078d 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -253,31 +253,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") } @@ -292,32 +292,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") } } @@ -331,7 +331,7 @@ 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) } @@ -355,7 +355,7 @@ 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) } @@ -383,7 +383,7 @@ func TestSpoofedLoginProtector(t *testing.T) { // Control case: protector with matching UID should be accepted. protector := getFakeLoginProtector(myUID) - if err = mnt.AddProtector(protector); err != nil { + if err = mnt.AddProtector(protector, nil); err != nil { t.Fatal(err) } _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) @@ -398,7 +398,7 @@ func TestSpoofedLoginProtector(t *testing.T) { // *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); err != nil { + if err = mnt.AddProtector(protector, nil); err != nil { t.Fatal(err) } _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil) @@ -445,19 +445,19 @@ 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 var isNewLink bool - if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil); err != nil { + 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); err != nil { + if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil, nil); err != nil { t.Fatal(err) } if isNewLink { -- cgit v1.2.3 From 06c989df4e31dd9f172f94fbd6243f49d4dd0b92 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: filesystem: create metadata files with mode 0600 Currently, fscrypt policies and protectors are world readable, as they are created with mode 0644. While this can be nice for use cases where users share these files, those use cases seem to be quite rare, and it's not a great default security-wise since it exposes password hashes to all users. While fscrypt uses a very strong password hash algorithm, it would still be best to follow the lead of /etc/shadow and keep this information non-world-readable. Therefore, start creating these files with mode 0600. Of course, if users do actually want to share these files, they have the option of simply chmod'ing them to a less restrictive mode. An option could also be added to make fscrypt use the old mode 0644; however, the need for that is currently unclear. --- filesystem/filesystem_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'filesystem/filesystem_test.go') diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go index f74078d..0e15256 100644 --- a/filesystem/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -413,6 +413,41 @@ func TestSpoofedLoginProtector(t *testing.T) { } } +// 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 { -- cgit v1.2.3