aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions/context.go11
-rw-r--r--actions/policy.go9
-rw-r--r--cmd/fscrypt/commands.go80
-rw-r--r--cmd/fscrypt/errors.go6
-rw-r--r--cmd/fscrypt/flags.go8
-rw-r--r--cmd/fscrypt/fscrypt.go33
-rw-r--r--crypto/crypto.go4
-rw-r--r--crypto/crypto_test.go18
-rw-r--r--crypto/key.go47
-rw-r--r--pam/pam.go16
-rw-r--r--security/keyring.go159
-rw-r--r--security/privileges.go120
-rw-r--r--util/util.go17
13 files changed, 426 insertions, 102 deletions
diff --git a/actions/context.go b/actions/context.go
index fb25b54..7e4b64b 100644
--- a/actions/context.go
+++ b/actions/context.go
@@ -31,9 +31,10 @@ package actions
import (
"log"
+ "golang.org/x/sys/unix"
+
"github.com/pkg/errors"
- "github.com/google/fscrypt/crypto"
"github.com/google/fscrypt/filesystem"
"github.com/google/fscrypt/metadata"
)
@@ -101,8 +102,10 @@ func (ctx *Context) checkContext() error {
}
// getService returns the keyring service for this context. We use the presence
-// of the LegacyConfig flag to determine if we should use the legacy services
-// (which are necessary for kernels before v4.8).
+// of the LegacyConfig flag to determine if we should use the legacy services.
+// For ext4 systems before v4.8 and f2fs systems before v4.6, filesystem
+// specific services must be used (these legacy services will still work with
+// later kernels).
func (ctx *Context) getService() string {
// For legacy configurations, we may need non-standard services
if ctx.Config.HasCompatibilityOption(LegacyConfig) {
@@ -111,7 +114,7 @@ func (ctx *Context) getService() string {
return ctx.Mount.Filesystem + ":"
}
}
- return crypto.DefaultService
+ return unix.FS_KEY_DESC_PREFIX
}
// getProtectorOption returns the ProtectorOption for the protector on the
diff --git a/actions/policy.go b/actions/policy.go
index ceae573..bf1f593 100644
--- a/actions/policy.go
+++ b/actions/policy.go
@@ -29,6 +29,7 @@ import (
"github.com/google/fscrypt/crypto"
"github.com/google/fscrypt/filesystem"
"github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/security"
"github.com/google/fscrypt/util"
)
@@ -56,10 +57,10 @@ func PurgeAllPolicies(ctx *Context) error {
for _, policyDescriptor := range policies {
service := ctx.getService()
- err = crypto.RemovePolicyKey(service + policyDescriptor)
+ err = security.RemoveKey(service + policyDescriptor)
switch errors.Cause(err) {
- case nil, crypto.ErrKeyringSearch:
+ case nil, security.ErrKeyringSearch:
// We don't care if the key has already been removed
default:
return err
@@ -365,7 +366,7 @@ func (policy *Policy) Apply(path string) error {
// IsProvisioned returns a boolean indicating if the policy has its key in the
// keyring, meaning files and directories using this policy are accessible.
func (policy *Policy) IsProvisioned() bool {
- _, err := crypto.FindPolicyKey(policy.Description())
+ _, err := security.FindKey(policy.Description())
return err == nil
}
@@ -381,7 +382,7 @@ func (policy *Policy) Provision() error {
// Deprovision removes the Policy key from the kernel keyring. This prevents
// reading and writing to the directory once the caches are cleared.
func (policy *Policy) Deprovision() error {
- return crypto.RemovePolicyKey(policy.Description())
+ return security.RemoveKey(policy.Description())
}
// commitData writes the Policy's current data to the filesystem.
diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go
index 4f53fe2..e6c7f9a 100644
--- a/cmd/fscrypt/commands.go
+++ b/cmd/fscrypt/commands.go
@@ -25,12 +25,15 @@ import (
"log"
"os"
+ "golang.org/x/sys/unix"
+
"github.com/pkg/errors"
"github.com/urfave/cli"
"github.com/google/fscrypt/actions"
"github.com/google/fscrypt/filesystem"
"github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/util"
)
// Setup is a command which can to global or per-filesystem initialization.
@@ -304,27 +307,36 @@ func unlockAction(c *cli.Context) error {
var Purge = cli.Command{
Name: "purge",
ArgsUsage: mountpointArg,
- Usage: "[EXPERIMENTAL] remove a filesystem's keys",
- Description: fmt.Sprintf(`EXPERIMENTAL: This command removes all the
- policy keys for directories on %[1]s. This is intended to lock
- all encrypted files and directories on %[1]s, in that unlocking
+ Usage: "Remove a filesystem's keys",
+ Description: fmt.Sprintf(`This command removes a user's policy keys for
+ directories on %[1]s. This is intended to lock all files and
+ directories encrypted by the user on %[1]s, in that unlocking
them for reading will require providing a key again. However,
- this action is currently subject to two significant limitations:
-
- (1) If "fscrypt purge" is run, but the filesystem has not yet
- been unmounted, recently accessed encrypted directories and
- files will remain accessible for some time. Because of this,
- after purging a filesystem's keys, it is recommended to unmount
- the filesystem. This limitation might be eliminated in a future
- kernel version.
-
- (2) Even after unmounting the filesystem, the kernel may keep
- contents of encrypted files cached in memory. This means direct
- memory access (either though physical compromise or a kernel
- exploit) could compromise encrypted data. This weakness can be
- eliminated by cycling the power or mitigated by using page cache
- and slab cache poisoning.`, mountpointArg),
- Flags: []cli.Flag{forceFlag},
+ there are four important things to note about this command:
+
+ (1) When run with the default options, this command also clears
+ the dentry and inode cache, so that the encrypted files and
+ directories will no longer be visible. However, this requires
+ root privileges.
+
+ (2) When run with %[2]s=false, the keyring is cleared and root
+ permissions are not required, but recently accessed encrypted
+ directories and files will remain cached for some time. Because
+ of this, after purging a filesystem's keys, it is recommended to
+ unmount the filesystem.
+
+ (3) When run as root, this command removes the policy keys for
+ all users. However, this will only work if the PAM module has
+ been enabled. Otherwise, only root's keys may be removed.
+
+ (4) Even after unmounting the filesystem or clearing the
+ caches, the kernel may keep contents of files in memory. This
+ means direct memory access (either though physical compromise or
+ a kernel exploit) could compromise encrypted data. This weakness
+ can be eliminated by cycling the power or mitigated by using
+ page cache and slab cache poisoning.`, mountpointArg,
+ shortDisplay(dropCachesFlag)),
+ Flags: []cli.Flag{forceFlag, dropCachesFlag},
Action: purgeAction,
}
@@ -333,25 +345,39 @@ func purgeAction(c *cli.Context) error {
return expectedArgsErr(c, 1, false)
}
+ if dropCachesFlag.Value {
+ if unix.Geteuid() != 0 {
+ return newExitError(c, ErrDropCachesPerm)
+ }
+ }
+
ctx, err := actions.NewContextFromMountpoint(c.Args().Get(0))
if err != nil {
return newExitError(c, err)
}
- err = askConfirmation(fmt.Sprintf(
- "Purge all policy keys from %q?",
- ctx.Mount.Path), false,
- "Encrypted data on this filesystem will be inaccessible until unlocked again!!")
- if err != nil {
+ question := fmt.Sprintf("Purge all policy keys from %q", ctx.Mount.Path)
+ if dropCachesFlag.Value {
+ question += " and drop global inode cache"
+ }
+ warning := "Encrypted data on this filesystem will be inaccessible until unlocked again!!"
+ if err = askConfirmation(question+"?", false, warning); err != nil {
return newExitError(c, err)
}
if err = actions.PurgeAllPolicies(ctx); err != nil {
return newExitError(c, err)
}
+ fmt.Fprintf(c.App.Writer, "Policies purged for %q.\n", ctx.Mount.Path)
- fmt.Fprintf(c.App.Writer, "All keys purged for %q.\n", ctx.Mount.Path)
- fmt.Fprintf(c.App.Writer, "Filesystem %q should now be unmounted.\n", ctx.Mount.Path)
+ if dropCachesFlag.Value {
+ if err = util.DropInodeCache(); err != nil {
+ return newExitError(c, err)
+ }
+ fmt.Fprintf(c.App.Writer, "Global inode cache cleared.\n")
+ } else {
+ fmt.Fprintf(c.App.Writer, "Filesystem %q should now be unmounted.\n", ctx.Mount.Path)
+ }
return nil
}
diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go
index b0548d5..10dbf1e 100644
--- a/cmd/fscrypt/errors.go
+++ b/cmd/fscrypt/errors.go
@@ -58,6 +58,7 @@ var (
ErrNotEmptyDir = errors.New("not an empty directory")
ErrNotPassphrase = errors.New("protector does not use a passphrase")
ErrUnknownUser = errors.New("unknown user")
+ ErrDropCachesPerm = errors.New("inode cache can only be dropped as root")
)
var loadHelpText = fmt.Sprintf("You may need to mount a linked filesystem. Run with %s for more information.", shortDisplay(verboseFlag))
@@ -112,6 +113,11 @@ func getErrorSuggestions(err error) string {
cannot be encrypted in-place. Instead, encrypt an empty
directory, copy the files into that encrypted directory,
and securely delete the originals with "shred".`
+ case ErrDropCachesPerm:
+ return fmt.Sprintf(`Either this command should be run as root to
+ properly clear the inode cache, or it should be run with
+ %s=false (this may leave encrypted files and directories
+ in an accessible state).`, shortDisplay(dropCachesFlag))
case ErrAllLoadsFailed:
return loadHelpText
default:
diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go
index d54a3bd..a06b952 100644
--- a/cmd/fscrypt/flags.go
+++ b/cmd/fscrypt/flags.go
@@ -158,6 +158,14 @@ var (
"fscrypt unlock" will need to be run in order to use the
directory.`,
}
+ dropCachesFlag = &boolFlag{
+ Name: "drop-caches",
+ Usage: `After purging the keys from the keyring, drop the
+ inode and dentry cache for the purge to take effect.
+ Without this flag, cached encrypted files may still have
+ their plaintext visible. Requires root privileges.`,
+ Default: true,
+ }
)
// Option flags: used to specify options instead of being prompted for them
diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go
index fc93c05..fe1e0c9 100644
--- a/cmd/fscrypt/fscrypt.go
+++ b/cmd/fscrypt/fscrypt.go
@@ -28,8 +28,12 @@ import (
"io/ioutil"
"log"
"os"
+ "strconv"
"time"
+ "golang.org/x/sys/unix"
+
+ "github.com/google/fscrypt/security"
"github.com/urfave/cli"
)
@@ -99,7 +103,7 @@ func setupCommand(command *cli.Command) {
// Setup function handlers
command.OnUsageError = onUsageError
if len(command.Subcommands) == 0 {
- command.Before = setupOutputs
+ command.Before = setupBefore
} else {
// Cleanup subcommands (if applicable)
for i := range command.Subcommands {
@@ -108,10 +112,12 @@ func setupCommand(command *cli.Command) {
}
}
-// setupOutputs makes sure our logs, errors, and output are going to the correct
+// setupBefore makes sure our logs, errors, and output are going to the correct
// io.Writers and that we haven't over-specified our flags. We only print the
// logs when using verbose, and only print normal stuff when not using quiet.
-func setupOutputs(c *cli.Context) error {
+// When running with sudo, this function also verifies that we have the proper
+// keyring linkage enabled.
+func setupBefore(c *cli.Context) error {
log.SetOutput(ioutil.Discard)
c.App.Writer = ioutil.Discard
@@ -121,6 +127,27 @@ func setupOutputs(c *cli.Context) error {
if !quietFlag.Value {
c.App.Writer = os.Stdout
}
+
+ if unix.Geteuid() != 0 {
+ return nil // Must be root to setup links
+ }
+ euid, err := strconv.Atoi(os.Getenv("SUDO_UID"))
+ if err != nil {
+ return nil // Must be running with sudo
+ }
+ egid, err := strconv.Atoi(os.Getenv("SUDO_GID"))
+ if err != nil {
+ return nil // Must be running with sudo
+ }
+
+ // Dropping and raising privileges checks the needed keyring link.
+ privs, err := security.DropThreadPrivileges(euid, egid)
+ if err != nil {
+ return newExitError(c, err)
+ }
+ if err := security.RaiseThreadPrivileges(privs); err != nil {
+ return newExitError(c, err)
+ }
return nil
}
diff --git a/crypto/crypto.go b/crypto/crypto.go
index b6368ce..62226b9 100644
--- a/crypto/crypto.go
+++ b/crypto/crypto.go
@@ -20,7 +20,6 @@
// Package crypto manages all the cryptography for fscrypt. This includes:
// - Key management (key.go)
// - Securely holding keys in memory
-// - Inserting keys into the keyring
// - Making recovery keys
// - Randomness (rand.go)
// - Cryptographic algorithms (crypto.go)
@@ -63,9 +62,6 @@ var (
ErrGetrandomFail = util.SystemError("getrandom() failed")
ErrKeyAlloc = util.SystemError("could not allocate memory for key")
ErrKeyFree = util.SystemError("could not free memory of key")
- ErrKeyringInsert = util.SystemError("could not insert key into the keyring")
- ErrKeyringSearch = errors.New("could not find key with descriptor")
- ErrKeyringDelete = util.SystemError("could not delete key from the keyring")
)
// panicInputLength panics if "name" has invalid length (expected != actual)
diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go
index 58aca9e..a069b1b 100644
--- a/crypto/crypto_test.go
+++ b/crypto/crypto_test.go
@@ -30,7 +30,10 @@ import (
"os"
"testing"
+ "golang.org/x/sys/unix"
+
"github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/security"
)
// Reader that always returns the same byte
@@ -52,6 +55,7 @@ var (
fakeValidDescriptor = "0123456789abcdef"
fakeSalt = bytes.Repeat([]byte{'a'}, metadata.SaltLen)
fakePassword = []byte("password")
+ defaultService = unix.FS_KEY_DESC_PREFIX
fakeValidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen)
fakeInvalidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen-1)
@@ -237,12 +241,12 @@ func TestKeyLargeResize(t *testing.T) {
// Adds and removes a key with various services.
func TestAddRemoveKeys(t *testing.T) {
- for _, service := range []string{DefaultService, "ext4:", "f2fs:"} {
+ for _, service := range []string{defaultService, "ext4:", "f2fs:"} {
validDescription := service + fakeValidDescriptor
if err := InsertPolicyKey(fakeValidPolicyKey, validDescription); err != nil {
t.Error(err)
}
- if err := RemovePolicyKey(validDescription); err != nil {
+ if err := security.RemoveKey(validDescription); err != nil {
t.Error(err)
}
}
@@ -250,24 +254,24 @@ func TestAddRemoveKeys(t *testing.T) {
// Adds a key twice (both should succeed)
func TestAddTwice(t *testing.T) {
- validDescription := DefaultService + fakeValidDescriptor
+ validDescription := defaultService + fakeValidDescriptor
InsertPolicyKey(fakeValidPolicyKey, validDescription)
if InsertPolicyKey(fakeValidPolicyKey, validDescription) != nil {
t.Error("InsertPolicyKey should not fail if key already exists")
}
- RemovePolicyKey(validDescription)
+ security.RemoveKey(validDescription)
}
// Makes sure a key fails with bad policy or service
func TestBadAddKeys(t *testing.T) {
- validDescription := DefaultService + fakeValidDescriptor
+ validDescription := defaultService + fakeValidDescriptor
if InsertPolicyKey(fakeInvalidPolicyKey, validDescription) == nil {
- RemovePolicyKey(validDescription)
+ security.RemoveKey(validDescription)
t.Error("InsertPolicyKey should fail with bad policy key")
}
invalidDescription := "ext4" + fakeValidDescriptor
if InsertPolicyKey(fakeValidPolicyKey, invalidDescription) == nil {
- RemovePolicyKey(invalidDescription)
+ security.RemoveKey(invalidDescription)
t.Error("InsertPolicyKey should fail with bad service")
}
}
diff --git a/crypto/key.go b/crypto/key.go
index 1d9e72c..656e6dc 100644
--- a/crypto/key.go
+++ b/crypto/key.go
@@ -40,20 +40,11 @@ import (
"golang.org/x/sys/unix"
"github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/security"
"github.com/google/fscrypt/util"
)
const (
- // DefaultService is the service which should be used for all encryption
- // keys unless not possible for legacy reasons. For ext4 systems before
- // v4.8 and f2fs systems before v4.6, filesystem specific services must
- // be used (these legacy services will still work with later kernels).
- DefaultService = unix.FS_KEY_DESC_PREFIX
- // KeyringID is the keyring that fscrypt's keys will be added to. Currently it
- // is the user keyring to avoid hitting systemd/issues/5715.
- KeyringID = unix.KEY_SPEC_USER_KEYRING
- // keyType is always logon as required by filesystem encryption
- keyType = "logon"
// Keys need to readable and writable, but hidden from other processes.
keyProtection = unix.PROT_READ | unix.PROT_WRITE
keyMmapFlags = unix.MAP_PRIVATE | unix.MAP_ANONYMOUS
@@ -252,34 +243,6 @@ func NewFixedLengthKeyFromReader(reader io.Reader, length int) (*Key, error) {
return key, nil
}
-// FindPolicyKey tries to locate a policy key in the kernel keyring with the
-// provided description. The keyring and key ids are returned if we can find the
-// key. An error is returned if the key does not exist.
-func FindPolicyKey(description string) (keyID int, err error) {
- keyID, err = unix.KeyctlSearch(KeyringID, keyType, description, 0)
- log.Printf("unix.KeyctlSearch(%d, %s, %s) = %d, %v", KeyringID, keyType, description, keyID, err)
- if err != nil {
- err = errors.Wrap(ErrKeyringSearch, err.Error())
- }
- return
-}
-
-// RemovePolicyKey tries to remove a policy key from the kernel keyring with the
-// provided description. An error is returned if the key does not exist.
-func RemovePolicyKey(description string) error {
- keyID, err := FindPolicyKey(description)
- if err != nil {
- return err
- }
-
- _, err = unix.KeyctlInt(unix.KEYCTL_UNLINK, keyID, KeyringID, 0, 0)
- log.Printf("unix.KeyctlUnlink(%d, %d) = %v", keyID, KeyringID, err)
- if err != nil {
- return errors.Wrap(ErrKeyringDelete, err.Error())
- }
- return nil
-}
-
// InsertPolicyKey puts the provided policy key into the kernel keyring with the
// provided description, and type logon. The key must be a policy key.
func InsertPolicyKey(key *Key, description string) error {
@@ -301,13 +264,7 @@ func InsertPolicyKey(key *Key, description string) error {
fscryptKey.Size = metadata.PolicyKeyLen
copy(fscryptKey.Raw[:], key.data)
- keyID, err := unix.AddKey(keyType, description, payload.data, KeyringID)
- log.Printf("unix.AddKey(%s, %s, <payload>, %d) = %d, %v",
- keyType, description, KeyringID, keyID, err)
- if err != nil {
- return errors.Wrap(ErrKeyringInsert, err.Error())
- }
- return nil
+ return security.InsertKey(payload.data, description)
}
var (
diff --git a/pam/pam.go b/pam/pam.go
index e928883..9188b6e 100644
--- a/pam/pam.go
+++ b/pam/pam.go
@@ -140,19 +140,19 @@ func (h *Handle) GetItem(i Item) (unsafe.Pointer, error) {
return data, h.err()
}
-// GetUID retrieves the UID of the corresponding PAM_USER.
-func (h *Handle) GetUID() (int64, error) {
+// GetIDs retrieves the UID and GID of the corresponding PAM_USER.
+func (h *Handle) GetIDs() (uid int, gid int, err error) {
var pamUsername *C.char
h.status = C.pam_get_user(h.handle, &pamUsername, nil)
- if err := h.err(); err != nil {
- return 0, err
+ if err = h.err(); err != nil {
+ return 0, 0, err
}
- pwd := C.getpwnam(pamUsername)
- if pwd == nil {
- return 0, fmt.Errorf("unknown user %q", C.GoString(pamUsername))
+ pwnam := C.getpwnam(pamUsername)
+ if pwnam == nil {
+ return 0, 0, fmt.Errorf("unknown user %q", C.GoString(pamUsername))
}
- return int64(pwd.pw_uid), nil
+ return int(pwnam.pw_uid), int(pwnam.pw_gid), nil
}
func (h *Handle) err() error {
diff --git a/security/keyring.go b/security/keyring.go
new file mode 100644
index 0000000..e312df2
--- /dev/null
+++ b/security/keyring.go
@@ -0,0 +1,159 @@
+/*
+ * privileges.go - Handles inserting/removing into user keyrings.
+ *
+ * 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 security
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "strconv"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+)
+
+const (
+ // file which lists all visible keys
+ keyListFilename = "/proc/keys"
+ // keyType is always logon as required by filesystem encryption.
+ keyType = "logon"
+)
+
+// FindKey tries to locate a key in the kernel keyring with the provided
+// description. The key id is returned if we can find the key. An error is
+// returned if the key does not exist.
+func FindKey(description string) (int, error) {
+ keyringID, err := getUserKeyringID()
+ if err != nil {
+ return 0, err
+ }
+
+ keyID, err := unix.KeyctlSearch(keyringID, keyType, description, 0)
+ log.Printf("KeyctlSearch(%d, %s, %s) = %d, %v", keyringID, keyType, description, keyID, err)
+ if err != nil {
+ return 0, errors.Wrap(ErrKeyringSearch, err.Error())
+ }
+ return keyID, err
+}
+
+// RemoveKey tries to remove a policy key from the kernel keyring with the
+// provided description. An error is returned if the key does not exist.
+func RemoveKey(description string) error {
+ keyID, err := FindKey(description)
+ if err != nil {
+ return err
+ }
+
+ // We use KEYCTL_INVALIDATE instead of KEYCTL_REVOKE because
+ // invalidating a key immediately removes it.
+ _, err = unix.KeyctlInt(unix.KEYCTL_INVALIDATE, keyID, 0, 0, 0)
+ log.Printf("KeyctlInvalidate(%d) = %v", keyID, err)
+ if err != nil {
+ return errors.Wrap(ErrKeyringDelete, err.Error())
+ }
+ return nil
+}
+
+// InsertKey puts the provided data into the kernel keyring with the provided
+// description.
+func InsertKey(data []byte, description string) error {
+ keyringID, err := getUserKeyringID()
+ if err != nil {
+ return err
+ }
+
+ keyID, err := unix.AddKey(keyType, description, data, keyringID)
+ log.Printf("KeyctlAddKey(%s, %s, <data>, %d) = %d, %v",
+ keyType, description, keyringID, keyID, err)
+ if err != nil {
+ return errors.Wrap(ErrKeyringInsert, err.Error())
+ }
+ return nil
+}
+
+var keyringIDCache = make(map[int]int)
+
+// getUserKeyringID returns the key id of the current user's user keyring. A
+// simpler approach would be to use
+// unix.KeyctlGetKeyringID(unix.KEY_SPEC_USER_KEYRING, false)
+// which would work in almost all cases. However, despite the fact that the rest
+// of the keyrings API using the _effective_ UID throughout, the translation of
+// KEY_SPEC_USER_KEYRING is done with respect to the _real_ UID. This means that
+// a simpler implementation would not respect permissions dropping.
+func getUserKeyringID() (int, error) {
+ // We will cache the result of this function.
+ euid := unix.Geteuid()
+ if keyringID, ok := keyringIDCache[euid]; ok {
+ return keyringID, nil
+ }
+
+ data, err := ioutil.ReadFile(keyListFilename)
+ if err != nil {
+ log.Print(err)
+ return 0, ErrReadingKeyList
+ }
+
+ expectedName := fmt.Sprintf("_uid.%d:", euid)
+ for _, line := range bytes.Split(data, []byte{'\n'}) {
+ if len(line) == 0 {
+ continue
+ }
+
+ // Each line in /proc/keys should have 9 columns.
+ columns := bytes.Fields(line)
+ if len(columns) < 9 {
+ return 0, ErrReadingKeyList
+ }
+ hexID := string(columns[0])
+ owner := string(columns[5])
+ name := string(columns[8])
+
+ // Our desired key is owned by the user and has the right name.
+ // The owner check is so another user cannot DOS this user by
+ // inserting a keyring with a similar name.
+ if owner != strconv.Itoa(euid) || name != expectedName {
+ continue
+ }
+
+ // The keyring's ID is encoded as hex.
+ parsedID, err := strconv.ParseInt(hexID, 16, 32)
+ if err != nil {
+ log.Print(err)
+ return 0, ErrReadingKeyList
+ }
+
+ keyringID := int(parsedID)
+ keyringIDCache[euid] = keyringID
+ return keyringID, nil
+ }
+
+ return 0, ErrFindingKeyring
+}
+
+func keyringLink(keyID int, keyringID int) error {
+ _, err := unix.KeyctlInt(unix.KEYCTL_LINK, keyID, keyringID, 0, 0)
+ return errors.Wrapf(err, "linking key %d into keyring %d", keyID, keyringID)
+}
+
+func keyringUnlink(keyID int, keyringID int) error {
+ _, err := unix.KeyctlInt(unix.KEYCTL_UNLINK, keyID, keyringID, 0, 0)
+ return errors.Wrapf(err, "unlinking key %d from keyring %d", keyID, keyringID)
+}
diff --git a/security/privileges.go b/security/privileges.go
new file mode 100644
index 0000000..cdabe71
--- /dev/null
+++ b/security/privileges.go
@@ -0,0 +1,120 @@
+/*
+ * privileges.go - Handles raising and dropping user privileges.
+ *
+ * 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 security manages:
+// - Keyring Operations (keyring.go)
+// - Privilege manipulation (privileges.go)
+// - Maintaining the link between the root and user keyrings.
+package security
+
+import (
+ "log"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+
+ "github.com/google/fscrypt/util"
+)
+
+// Package security error values
+var (
+ ErrReadingKeyList = util.SystemError("could not read keys from " + keyListFilename)
+ ErrFindingKeyring = util.SystemError("could not find user keyring")
+ ErrKeyringInsert = util.SystemError("could not insert key into the keyring")
+ ErrKeyringSearch = errors.New("could not find key with descriptor")
+ ErrKeyringDelete = util.SystemError("could not delete key from the keyring")
+)
+
+// Privileges contains the state needed to restore a user's original privileges.
+type Privileges struct {
+ euid int
+ egid int
+ groups []int
+}
+
+// DropThreadPrivileges temporarily drops the privileges of the current thread
+// to have the euid and egid specified. The returned opaque Privileges structure
+// should later be passed to RestoreThreadPrivileges.
+// Due to golang/go#1435, these privileges are only dropped for a single thread.
+// This function also makes sure that the appropriate user keyrings are linked.
+// This ensures that the user's keys are visible from commands like sudo.
+func DropThreadPrivileges(euid int, egid int) (*Privileges, error) {
+ var err error
+ privs := &Privileges{
+ euid: unix.Geteuid(),
+ egid: unix.Getegid(),
+ }
+ if privs.groups, err = unix.Getgroups(); err != nil {
+ return nil, errors.Wrapf(err, "getting groups")
+ }
+
+ // We link the privileged keyring into the thread keyring so that we
+ // can still modify it after dropping privileges.
+ privilegedUserKeyringID, err := getUserKeyringID()
+ if err != nil {
+ return nil, err
+ }
+ if err = keyringLink(privilegedUserKeyringID, unix.KEY_SPEC_THREAD_KEYRING); err != nil {
+ return nil, err
+ }
+ defer keyringUnlink(privilegedUserKeyringID, unix.KEY_SPEC_THREAD_KEYRING)
+
+ // Drop euid last so we have permissions to drop the others.
+ if err = unix.Setregid(-1, egid); err != nil {
+ return nil, errors.Wrapf(err, "dropping egid to %d", egid)
+ }
+ if err = unix.Setgroups([]int{egid}); err != nil {
+ return nil, errors.Wrapf(err, "dropping groups")
+ }
+ if err = unix.Setreuid(-1, euid); err != nil {
+ return nil, errors.Wrapf(err, "dropping euid to %d", euid)
+ }
+ log.Printf("privileges dropped to euid=%d, egid=%d", euid, egid)
+
+ // If the link already exists, this linking does nothing and succeeds.
+ droppedUserKeyringID, err := getUserKeyringID()
+ if err != nil {
+ return nil, err
+ }
+ if err = keyringLink(droppedUserKeyringID, privilegedUserKeyringID); err != nil {
+ return nil, err
+ }
+ log.Printf("user keyring (%d) linked into root user keyring (%d)",
+ droppedUserKeyringID, privilegedUserKeyringID)
+
+ return privs, nil
+}
+
+// RaiseThreadPrivileges returns the state of a threads privileges to what it
+// was before the call to DropThreadPrivileges.
+func RaiseThreadPrivileges(privs *Privileges) error {
+ // Raise euid last so we have permissions to raise the others.
+ if err := unix.Setreuid(-1, privs.euid); err != nil {
+ return errors.Wrapf(err, "raising euid to %d", privs.euid)
+ }
+ if err := unix.Setregid(-1, privs.egid); err != nil {
+ return errors.Wrapf(err, "raising egid to %d", privs.egid)
+ }
+ if err := unix.Setgroups(privs.groups); err != nil {
+ return errors.Wrapf(err, "raising groups")
+ }
+
+ log.Printf("privileges raised to euid=%d, egid=%d", privs.euid, privs.egid)
+ return nil
+}
diff --git a/util/util.go b/util/util.go
index 14d23e2..acdc3fc 100644
--- a/util/util.go
+++ b/util/util.go
@@ -25,6 +25,7 @@ package util
import (
"bufio"
+ "log"
"math"
"os"
"unsafe"
@@ -97,3 +98,19 @@ func ReadLine() (string, error) {
scanner.Scan()
return scanner.Text(), scanner.Err()
}
+
+// DropInodeCache instructs the kernel to clear the global cache of inodes and
+// dentries. This has the effect of making encrypted directories whose keys
+// are not present no longer accessible. Requires root privileges.
+func DropInodeCache() error {
+ log.Print("dropping page caches")
+ // See: https://www.kernel.org/doc/Documentation/sysctl/vm.txt
+ file, err := os.OpenFile("/proc/sys/vm/drop_caches", os.O_WRONLY|os.O_SYNC, 0)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ // "2" just clears the inodes and dentries
+ _, err = file.WriteString("2")
+ return err
+}