diff options
| author | Joe Richey joerichey@google.com <joerichey@google.com> | 2017-08-15 18:11:29 -0700 |
|---|---|---|
| committer | Joe Richey joerichey@google.com <joerichey@google.com> | 2017-08-17 22:49:44 -0700 |
| commit | 151e8965fa3a9c8f65e316430f9df0fa763fb02d (patch) | |
| tree | 5be6cb1e1d617e60ba7624abc3c940c65715ba5e | |
| parent | b4d51e0f4d34dbfd78e23662f3dfd90e86ae5e48 (diff) | |
cmd/fscrypt: purge command now clears cache
| -rw-r--r-- | actions/context.go | 11 | ||||
| -rw-r--r-- | actions/policy.go | 9 | ||||
| -rw-r--r-- | cmd/fscrypt/commands.go | 80 | ||||
| -rw-r--r-- | cmd/fscrypt/errors.go | 6 | ||||
| -rw-r--r-- | cmd/fscrypt/flags.go | 8 | ||||
| -rw-r--r-- | cmd/fscrypt/fscrypt.go | 33 | ||||
| -rw-r--r-- | crypto/crypto.go | 4 | ||||
| -rw-r--r-- | crypto/crypto_test.go | 18 | ||||
| -rw-r--r-- | crypto/key.go | 47 | ||||
| -rw-r--r-- | pam/pam.go | 16 | ||||
| -rw-r--r-- | security/keyring.go | 159 | ||||
| -rw-r--r-- | security/privileges.go | 120 | ||||
| -rw-r--r-- | util/util.go | 17 |
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 ( @@ -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 +} |