diff options
| author | Joseph Richey <joerichey@google.com> | 2017-08-24 00:53:11 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-08-24 00:53:11 -0700 |
| commit | 4879df9a6063886865b94c270660838060acbc20 (patch) | |
| tree | 9adaa99808990c0034484ed24d587c07ac70525d | |
| parent | 17794e94ebe140dc74f93abb8132f5295ee2004e (diff) | |
| parent | 19c13e861996c3503be5b0dc5a2cecfe186b1744 (diff) | |
fscrypt PAM module
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | CONTRIBUTING.md | 17 | ||||
| -rw-r--r-- | Makefile | 34 | ||||
| -rw-r--r-- | README.md | 80 | ||||
| -rw-r--r-- | actions/policy.go | 8 | ||||
| -rw-r--r-- | cmd/fscrypt/commands.go | 4 | ||||
| -rw-r--r-- | crypto/key.go | 1 | ||||
| -rw-r--r-- | pam/login.go | 4 | ||||
| -rw-r--r-- | pam/pam.go | 84 | ||||
| -rw-r--r-- | pam_fscrypt/config | 13 | ||||
| -rw-r--r-- | pam_fscrypt/pam_fscrypt.go | 286 | ||||
| -rw-r--r-- | pam_fscrypt/run_fscrypt.go | 210 | ||||
| -rw-r--r-- | security/cache.go | 41 | ||||
| -rw-r--r-- | security/keyring.go | 19 | ||||
| -rw-r--r-- | security/privileges.go | 1 | ||||
| -rw-r--r-- | util/util.go | 25 |
16 files changed, 734 insertions, 97 deletions
@@ -1,2 +1,4 @@ -/fscrypt +fscrypt +fscrypt.* fscrypt_image +pam_fscrypt.so diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1470fa4..357661c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,16 @@ You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. +## Reporting an Issue + +Any bugs or problems found in fscrypt should be reported though the +[Github Issue Tracker](https://github.com/google/fscrypt/issues/new). When +reporting an issue, be sure to give as much information about the problem as +possible. If reporting an issue around the fscrypt command-line tool, post the +relevant output from fscrypt, running with the `--verbose` flag. For the +pam_fscrypt module, use the `debug` option with the module and post the relevant +parts of the syslog (usually at `/var/log/syslog`). + ## Code reviews All submissions, including submissions by project members, require review. We @@ -30,9 +40,10 @@ these commands when writing your code. ### Building and Testing -As mentioned in `README.md`, running `make` will build the fscrypt executable. -Running `make go` will build each package and run the tests, but just running -`make go` with nothing else will skip the integration tests. +As mentioned in `README.md`, running `make` will build the fscrypt executable +and the PAM module `pam_fscrypt.so`. Running `make go` will build each package +and run the tests, but just running `make go` with nothing else will skip the +integration tests. To run the integration tests, you will need a filesystem that supports encryption. If you already have some empty filesystem at `/foo/bar`, just run: @@ -16,11 +16,16 @@ # the License. NAME = fscrypt +PAM_NAME = pam_$(NAME) +PAM_MODULE = $(PAM_NAME).so -INSTALL = install -DESTDIR = /usr/local/bin +INSTALL ?= install +DESTDIR ?= /usr/local/bin +PAM_MODULE_DIR ?= /lib/security +PAM_CONFIG_DIR ?= /usr/share/pam-configs CMD_PKG = github.com/google/$(NAME)/cmd/$(NAME) +PAM_PKG = github.com/google/$(NAME)/$(PAM_NAME) SRC_FILES = $(shell find . -type f -name '*.go' -o -name "*.h" -o -name "*.c") GO_FILES = $(shell find . -type f -name '*.go' -not -path "./vendor/*") @@ -81,15 +86,20 @@ override GO_LINK_FLAGS += $(VERSION_FLAG) $(DATE_FLAG) -extldflags "$(LDFLAGS)" override GO_FLAGS += --ldflags '$(GO_LINK_FLAGS)' .PHONY: default all -default: $(NAME) + +default: $(NAME) $(PAM_MODULE) all: update format lint default test $(NAME): $(SRC_FILES) go build $(GO_FLAGS) -o $(NAME) $(CMD_PKG) +$(PAM_MODULE): $(SRC_FILES) + go build -buildmode=c-shared $(GO_FLAGS) -o $(PAM_MODULE) $(PAM_PKG) + rm -f $(PAM_NAME).h + .PHONY: clean clean: - rm -rf $(NAME) $(IMAGE) + rm -f $(NAME) $(PAM_MODULE) $(IMAGE) # Make sure go files build and tests pass. .PHONY: test @@ -129,14 +139,22 @@ lint: @golint $(GO_PKGS) | grep -v "pb.go" | ./input_fail.py @megacheck -unused.exported $(GO_PKGS) -.PHONY: install -install: $(NAME) +###### Installation commands ##### +.PHONY: install_bin install_pam install uninstall +install_bin: $(NAME) $(INSTALL) -d $(DESTDIR) $(INSTALL) $(NAME) $(DESTDIR) -.PHONY: uninstall +install_pam: $(PAM_MODULE) + $(INSTALL) -d $(PAM_MODULE_DIR) + $(INSTALL) $(PAM_MODULE) $(PAM_MODULE_DIR) + $(INSTALL) -d $(PAM_CONFIG_DIR) + $(INSTALL) $(PAM_NAME)/config $(PAM_CONFIG_DIR)/$(NAME) + +install: install_bin install_pam + uninstall: - rm -rf $(DESTDIR)/$(NAME) + rm -f $(DESTDIR)/$(NAME) $(PAM_MODULE_DIR)/$(PAM_MODULE) $(PAM_CONFIG_DIR)/$(NAME) # Install the go tools used for checking/generating the code .PHONY: go-tools @@ -99,7 +99,6 @@ The following functionality is planned: * `fscrypt backup` - Manages backups of the fscrypt metadata * `fscrypt recovery` - Manages recovery keys for directories * `fscrypt cleanup` - Scans filesystem for unused policies/protectors -* A PAM module to support login passphrase changes (see below) See the example usage section below or run `fscrypt COMMAND --help` for more information about each of the commands. @@ -109,7 +108,7 @@ information about each of the commands. fscrypt has the following build dependencies: * [Go](https://golang.org/doc/install) * A C compiler (`gcc` or `clang`) -* `make` +* `make` * The [Argon2 Passphrase Hash](https://github.com/P-H-C/phc-winner-argon2) library, which can be [directly installed on Artful Ubuntu](https://packages.ubuntu.com/artful/libargon2-0-dev), @@ -131,7 +130,7 @@ Once all the dependencies are installed, you can get the repository by running: go get -d github.com/google/fscrypt/... ``` and then you can run `make` in `$GOPATH/github.com/google/fscrypt` to build the -executable in that directory. Running `sudo make install` installs the binary to +executable and PAM moudle in that directory. Running `sudo make install` installs the binary to `/usr/local/bin`. See the `Makefile` for instructions on how to customize the build. This includes @@ -155,6 +154,51 @@ fscrypt has the following runtime dependencies: The dynamic libraries are not needed if you built a static executable. +### Setting up the PAM module + +Note that to make use of the installed PAM module, your +[PAM configuration files](http://www.linux-pam.org/Linux-PAM-html/sag-configuration.html) +in `/etc/pam.d` must be modified to add fscrypt. + +#### Automatic setup on Ubuntu + +fscrypt automatically installs the +[PAM config file](https://wiki.ubuntu.com/PAMConfigFrameworkSpec) +`pam_fscrypt/config` to `/usr/share/pam-configs/fscrypt`. This file contains +reasonable defaults for the PAM module. To automatically apply these changes, +run `sudo pam-auth-update` and follow the on-screen instructions. + +#### Manual setup + +The fscrypt PAM module implements the Auth, Session, and Password +[types](http://www.linux-pam.org/Linux-PAM-html/sag-configuration-file.html). + +The Password functionality of `pam_fscrypt.so` is used to automatically rewrap +a user's login protector when their unix passphrase changes. An easy way to get +the working is to add the line: +``` +password optional pam_fscrypt.so +``` +after `pam_unix.so` in `/etc/pam.d/common-password` or similar. + +The Auth and Session functionality of `pam_fscrypt.so` are used to automatically +unlock directories when logging in as a user. An easy way to get this working is +to add the line: +``` +auth optional pam_fscrypt.so +``` +after `pam_unix.so` in `/etc/pam.d/common-password` or similar, and to add the +line: +``` +session optional pam_fscrypt.so drop_caches lock_policies +``` +after `pam_unix.so` in `/etc/pam.d/common-session` or similar. The +`lock_policies` option locks the directories protected with the user's login +passphrase when the last session ends. The `drop_caches` option tells fscrypt to +clear the filesystem caches when the last session closes, ensuring all the +locked data is inaccessible. All the types also support the `debug` option which +prints additional debug information to the syslog. + ## Note about stability fscrypt follows [semantic versioning](http://semver.org). As such, all versions @@ -502,23 +546,33 @@ file for more information about singing the CLA and submitting a pull request. ## Troubleshooting In general, if you are encountering issues with fscrypt, -[open an issue](https://github.com/google/fscrypt/issues/new). We will try our -best to help. +[open an issue](https://github.com/google/fscrypt/issues/new), following the +guidelines in `CONTRIBUTING.md`. We will try our best to help. #### I changed my login passphrase, now all my directories are inaccessible -We do not currently support the changing of the login passphrase. This will -change when the appropriate module is completed. Until then, you can fix it by -first finding the necessary protector (with `fscrypt status PATH`) and then -running: +The PAM module provided by fscrypt (`pam_fscrypt.so`) should automatically +detect changes to a user's login passphrase so that they can still access their +encrypted directories. However, sometimes the login passphrase can become +desynchronized from a user's login protector. This usually happens when the PAM +passphrase is managed by an external system, if the PAM module is not installed, +or if the PAM module is not properly configured. + +To fix your login protector, you first should find the appropriate protector ID +by running `fscrypt status "/"`. Then, change the passphrase for this protector +by running: ``` -fscrypt metadata change-passphrase --protector=MOUNTPOINT:ID +fscrypt metadata change-passphrase --protector=/:ID ``` -#### I can still see files or filenames after running `fscrypt purge MOUNTPOINT` +#### Directories using my login passphrase are not automatically unlocking. + +Either the PAM module is not installed correctly, or your login passphrase +changed and things got out of sync. Another reason that these directories might +not unlock is if your session starts without password authentication. The most +common case of this is public-key ssh login. -You need to unmount `MOUNTPOINT` to clear the necessary caches. See -`fscrypt purge --help` for more information +To trigger a password authentication event, run `su $(whoami) -c exit`. #### Getting "encryption not enabled" on an ext4 filesystem. diff --git a/actions/policy.go b/actions/policy.go index bf1f593..461f8cc 100644 --- a/actions/policy.go +++ b/actions/policy.go @@ -278,13 +278,19 @@ func (policy *Policy) Lock() error { return err } +// UsesProtector returns if the policy is protected with the protector +func (policy *Policy) UsesProtector(protector *Protector) bool { + _, ok := policy.findWrappedKeyIndex(protector.Descriptor()) + return ok +} + // AddProtector updates the data that is wrapping the Policy Key so that the // provided Protector is now protecting the specified Policy. If an error is // returned, no data has been changed. If the policy and protector are on // different filesystems, a link will be created between them. The policy and // protector must both be unlocked. func (policy *Policy) AddProtector(protector *Protector) error { - if _, ok := policy.findWrappedKeyIndex(protector.Descriptor()); ok { + if policy.UsesProtector(protector) { return ErrAlreadyProtected } if policy.key == nil || protector.key == nil { diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index e6c7f9a..3e8bc98 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -33,7 +33,7 @@ import ( "github.com/google/fscrypt/actions" "github.com/google/fscrypt/filesystem" "github.com/google/fscrypt/metadata" - "github.com/google/fscrypt/util" + "github.com/google/fscrypt/security" ) // Setup is a command which can to global or per-filesystem initialization. @@ -371,7 +371,7 @@ func purgeAction(c *cli.Context) error { fmt.Fprintf(c.App.Writer, "Policies purged for %q.\n", ctx.Mount.Path) if dropCachesFlag.Value { - if err = util.DropInodeCache(); err != nil { + if err = security.DropInodeCache(); err != nil { return newExitError(c, err) } fmt.Fprintf(c.App.Writer, "Global inode cache cleared.\n") diff --git a/crypto/key.go b/crypto/key.go index 656e6dc..497a0ef 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -175,7 +175,6 @@ func (key *Key) resize(requestedSize int) (*Key, error) { // string allocated by C. Note that this method is unsafe as this C copy has no // locking or wiping functionality. The key shouldn't contain any `\0` bytes. func (key *Key) UnsafeToCString() unsafe.Pointer { - // Memory for the key must be moved into a C string allocated by C. size := C.size_t(key.Len()) data := C.calloc(size+1, 1) C.memcpy(data, util.Ptr(key.data), size) diff --git a/pam/login.go b/pam/login.go index e89ee01..346edd4 100644 --- a/pam/login.go +++ b/pam/login.go @@ -38,7 +38,7 @@ import ( // Pam error values var ( - ErrPAMPassphrase = errors.New("incorrect login passphrase") + ErrPassphrase = errors.New("incorrect login passphrase") ) // Global state is needed for the PAM callback, so we guard this function with a @@ -107,7 +107,7 @@ func IsUserLoginToken(username string, token *crypto.Key, quiet bool) error { } if !authenticated { - return ErrPAMPassphrase + return ErrPassphrase } return nil } @@ -31,23 +31,43 @@ import "C" import ( "errors" "fmt" + "log" "unsafe" - "github.com/google/fscrypt/util" + "github.com/google/fscrypt/security" ) // Handle wraps the C pam_handle_t type. This is used from within modules. type Handle struct { handle *C.pam_handle_t status C.int + privs *security.Privileges + // UID of the user being authenticated + UID int + // GID of the user being authenticated + GID int } // NewHandle creates a Handle from a raw pointer. -func NewHandle(pamh unsafe.Pointer) *Handle { - return &Handle{ +func NewHandle(pamh unsafe.Pointer) (*Handle, error) { + h := &Handle{ handle: (*C.pam_handle_t)(pamh), status: C.PAM_SUCCESS, } + + var pamUsername *C.char + h.status = C.pam_get_user(h.handle, &pamUsername, nil) + if err := h.err(); err != nil { + return nil, err + } + + pwnam := C.getpwnam(pamUsername) + if pwnam == nil { + return nil, fmt.Errorf("unknown user %q", C.GoString(pamUsername)) + } + h.UID = int(pwnam.pw_uid) + h.GID = int(pwnam.pw_gid) + return h, nil } func (h *Handle) setData(name string, data unsafe.Pointer, cleanup C.CleanupFunc) error { @@ -99,39 +119,6 @@ func (h *Handle) GetString(name string) (string, error) { return C.GoString((*C.char)(data)), nil } -// SetSlice sets a []string value for the PAM data with the specified name. -func (h *Handle) SetSlice(name string, slice []string) error { - sliceLength := uintptr(len(slice)) - memorySize := (sliceLength + 1) * unsafe.Sizeof(uintptr(0)) - data := C.malloc(C.size_t(memorySize)) - - cSlice := util.PointerSlice(data) - for i, str := range slice { - cSlice[i] = unsafe.Pointer(C.CString(str)) - } - cSlice[sliceLength] = nil - - return h.setData(name, data, C.CleanupFunc(C.freeArray)) -} - -// GetSlice gets a []string value for the PAM data with the specified name. It -// should have been previously set with SetSlice(). -func (h *Handle) GetSlice(name string) ([]string, error) { - data, err := h.getData(name) - if err != nil { - return nil, err - } - - var slice []string - for _, cString := range util.PointerSlice(data) { - if cString == nil { - return slice, nil - } - slice = append(slice, C.GoString((*C.char)(cString))) - } - panic("We will never get here") -} - // GetItem retrieves a PAM information item. This a pointer directory to the // data, so it shouldn't be modified. func (h *Handle) GetItem(i Item) (unsafe.Pointer, error) { @@ -140,19 +127,22 @@ func (h *Handle) GetItem(i Item) (unsafe.Pointer, error) { return data, h.err() } -// 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, 0, err - } +// DropThreadPrivileges sets the effective privileges to that of the PAM user +func (h *Handle) DropThreadPrivileges() error { + var err error + h.privs, err = security.DropThreadPrivileges(h.UID, h.GID) + return err +} - pwnam := C.getpwnam(pamUsername) - if pwnam == nil { - return 0, 0, fmt.Errorf("unknown user %q", C.GoString(pamUsername)) +// RaiseThreadPrivileges restores the original privileges that were running the +// PAM module (this is usually root). As this error is often ignored in a defer +// statement, any error is also logged. +func (h *Handle) RaiseThreadPrivileges() error { + err := security.RaiseThreadPrivileges(h.privs) + if err != nil { + log.Print(err) } - return int(pwnam.pw_uid), int(pwnam.pw_gid), nil + return err } func (h *Handle) err() error { diff --git a/pam_fscrypt/config b/pam_fscrypt/config new file mode 100644 index 0000000..795a4f8 --- /dev/null +++ b/pam_fscrypt/config @@ -0,0 +1,13 @@ +Name: fscrypt PAM passphrase support +Default: yes +Priority: 0 +Auth-Type: Additional +Auth-Final: + optional pam_fscrypt.so +Session-Type: Additional +Session-Interactive-Only: yes +Session-Final: + optional pam_fscrypt.so drop_caches lock_policies +Password-Type: Additional +Password-Final: + optional pam_fscrypt.so diff --git a/pam_fscrypt/pam_fscrypt.go b/pam_fscrypt/pam_fscrypt.go new file mode 100644 index 0000000..21bc779 --- /dev/null +++ b/pam_fscrypt/pam_fscrypt.go @@ -0,0 +1,286 @@ +/* + * pam_fscrypt.go - Checks the validity of a login token key against PAM. + * + * 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 main + +/* +#cgo LDFLAGS: -lpam -fPIC + +#include <stdlib.h> +#include <string.h> + +#include <security/pam_appl.h> +*/ +import "C" +import ( + "log" + "unsafe" + + "github.com/pkg/errors" + + "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/crypto" + "github.com/google/fscrypt/pam" + "github.com/google/fscrypt/security" +) + +const ( + moduleName = "pam_fscrypt" + // authtokLabel tags the AUTHTOK in the PAM data. + authtokLabel = "fscrypt_authtok" + // These flags are used to toggle behavior of the PAM module. + debugFlag = "debug" + lockFlag = "lock_policies" + cacheFlag = "drop_caches" +) + +// Authenticate copies the AUTHTOK (if necessary) into the PAM data so it can be +// used in pam_sm_open_session. +func Authenticate(handle *pam.Handle, _ map[string]bool) error { + if err := handle.DropThreadPrivileges(); err != nil { + return err + } + defer handle.RaiseThreadPrivileges() + + // If this user doesn't have a login protector, no unlocking is needed. + if _, err := loginProtector(handle); err != nil { + log.Printf("no need to copy AUTHTOK: %s", err) + return nil + } + + log.Print("Authenticate: copying AUTHTOK for use in the session") + authtok, err := handle.GetItem(pam.Authtok) + if err != nil { + return errors.Wrap(err, "could not get AUTHTOK") + } + err = handle.SetSecret(authtokLabel, authtok) + return errors.Wrap(err, "could not set AUTHTOK data") +} + +// OpenSession provisions any policies protected with the login protector. +func OpenSession(handle *pam.Handle, _ map[string]bool) error { + // We will always clear the the AUTHTOK data + defer handle.ClearData(authtokLabel) + // Increment the count as we add a session + if _, err := AdjustCount(handle, +1); err != nil { + return err + } + + if err := handle.DropThreadPrivileges(); err != nil { + return err + } + defer handle.RaiseThreadPrivileges() + + // If there are no polices for the login protector, no unlocking needed. + protector, err := loginProtector(handle) + if err != nil { + log.Printf("nothing to unlock: %s", err) + return nil + } + policies := policiesUsingProtector(protector) + if len(policies) == 0 { + log.Print("no policies to unlock") + return nil + } + + log.Print("OpenSession: unlocking policies protected with AUTHTOK") + keyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) { + if retry { + // Login passphrase and login protector have diverged. + // We could prompt the user for the old passphrase and + // rewrap, but we currently don't. + return nil, pam.ErrPassphrase + } + + authtok, err := handle.GetSecret(authtokLabel) + if err != nil { + // pam_sm_authenticate was not run before the session is + // opened. This can happen when a user does something + // like "sudo su <user>". We could prompt for the + // login passphrase here, but we currently don't. + return nil, errors.Wrap(err, "AUTHTOK data missing") + } + + return crypto.NewKeyFromCString(authtok) + } + if err := protector.Unlock(keyFn); err != nil { + return errors.Wrapf(err, "unlocking protector %s", protector.Descriptor()) + } + defer protector.Lock() + + // We don't stop provisioning polices on error, we try all of them. + for _, policy := range policies { + if policy.IsProvisioned() { + log.Printf("policy %s already provisioned", policy.Descriptor()) + continue + } + if err := policy.UnlockWithProtector(protector); err != nil { + log.Printf("unlocking policy %s: %s", policy.Descriptor(), err) + continue + } + defer policy.Lock() + + if err := policy.Provision(); err != nil { + log.Printf("provisioning policy %s: %s", policy.Descriptor(), err) + continue + } + log.Printf("policy %s provisioned", policy.Descriptor()) + } + return nil +} + +// CloseSession can deprovision all keys provisioned at the start of the +// session. It can also clear the cache so these changes take effect. +func CloseSession(handle *pam.Handle, args map[string]bool) error { + // Only do stuff on session close when we are the last session + if count, err := AdjustCount(handle, -1); err != nil || count != 0 { + log.Printf("count is %d and we are not locking", count) + return err + } + + var errLock, errCache error + // Don't automatically drop privileges, we may need them to drop caches. + if args[lockFlag] { + log.Print("CloseSession: locking polices protected with login") + errLock = lockLoginPolicies(handle) + } + + if args[cacheFlag] { + log.Print("CloseSession: dropping inode caches") + errCache = security.DropInodeCache() + } + + if errLock != nil { + return errLock + } + return errCache +} + +// lockLoginPolicies deprovisions all policy keys that are protected by +// the user's login protector. +func lockLoginPolicies(handle *pam.Handle) error { + if err := handle.DropThreadPrivileges(); err != nil { + return err + } + defer handle.RaiseThreadPrivileges() + + // If there are no polices for the login protector, no locking needed. + protector, err := loginProtector(handle) + if err != nil { + log.Printf("nothing to lock: %s", err) + return nil + } + policies := policiesUsingProtector(protector) + if len(policies) == 0 { + log.Print("no policies to lock") + return nil + } + + // We will try to deprovision all of the policies. + for _, policy := range policies { + if !policy.IsProvisioned() { + log.Printf("policy %s not provisioned", policy.Descriptor()) + continue + } + if err := policy.Deprovision(); err != nil { + log.Printf("deprovisioning policy %s: %s", policy.Descriptor(), err) + continue + } + log.Printf("policy %s deprovisioned", policy.Descriptor()) + } + return nil +} + +// Chauthtok rewraps the login protector when the passphrase changes. +func Chauthtok(handle *pam.Handle, _ map[string]bool) error { + if err := handle.DropThreadPrivileges(); err != nil { + return err + } + defer handle.RaiseThreadPrivileges() + + protector, err := loginProtector(handle) + if err != nil { + log.Printf("nothing to rewrap: %s", err) + return nil + } + + oldKeyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) { + if retry { + // If the OLDAUTHTOK disagrees with the login protector, + // we do nothing, as the protector will (probably) still + // disagree after the login passphrase changes. + return nil, pam.ErrPassphrase + } + authtok, err := handle.GetItem(pam.Oldauthtok) + if err != nil { + return nil, errors.Wrap(err, "could not get OLDAUTHTOK") + } + return crypto.NewKeyFromCString(authtok) + } + + newKeyFn := func(_ actions.ProtectorInfo, _ bool) (*crypto.Key, error) { + authtok, err := handle.GetItem(pam.Authtok) + if err != nil { + return nil, errors.Wrap(err, "could not get AUTHTOK") + } + return crypto.NewKeyFromCString(authtok) + } + + log.Print("Chauthtok: rewrapping login protector") + if err = protector.Unlock(oldKeyFn); err != nil { + return err + } + defer protector.Lock() + + return protector.Rewrap(newKeyFn) +} + +//export pam_sm_authenticate +func pam_sm_authenticate(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { + return RunPamFunc(Authenticate, pamh, argc, argv) +} + +// pam_sm_stecred needed because we use pam_sm_authenticate. +//export pam_sm_setcred +func pam_sm_setcred(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { + return C.PAM_SUCCESS +} + +//export pam_sm_open_session +func pam_sm_open_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { + return RunPamFunc(OpenSession, pamh, argc, argv) +} + +//export pam_sm_close_session +func pam_sm_close_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { + return RunPamFunc(CloseSession, pamh, argc, argv) +} + +//export pam_sm_chauthtok +func pam_sm_chauthtok(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int { + // Only do rewrapping if we have both AUTHTOKs and a login protector. + if pam.Flag(flags)&pam.PrelimCheck != 0 { + return C.PAM_SUCCESS + } + + return RunPamFunc(Chauthtok, pamh, argc, argv) +} + +// main() is needed to make a shared library compile +func main() {} diff --git a/pam_fscrypt/run_fscrypt.go b/pam_fscrypt/run_fscrypt.go new file mode 100644 index 0000000..1527d42 --- /dev/null +++ b/pam_fscrypt/run_fscrypt.go @@ -0,0 +1,210 @@ +/* + * run_fscrypt.go - Helpers for running functions in the PAM module. + * + * 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 main + +/* +#cgo LDFLAGS: -lpam -fPIC + +#include <stdlib.h> +#include <string.h> + +#include <security/pam_appl.h> +*/ +import "C" +import ( + "fmt" + "io" + "io/ioutil" + "log" + "log/syslog" + "os" + "path/filepath" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/pkg/errors" + + "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/filesystem" + "github.com/google/fscrypt/metadata" + "github.com/google/fscrypt/pam" + "github.com/google/fscrypt/util" +) + +const ( + // countDirectory is in a tmpfs filesystem so it will reset on reboot. + countDirectory = "/run/fscrypt" + // count files should only be readable and writable by root + countDirectoryPermissions = 0700 + countFilePermissions = 0600 + countFileFormat = "%d\n" +) + +// PamFunc is used to define the various actions in the PAM module +type PamFunc func(handle *pam.Handle, args map[string]bool) error + +// RunPamFunc is used to convert between the Go functions and exported C funcs. +func RunPamFunc(f PamFunc, pamh unsafe.Pointer, argc C.int, argv **C.char) C.int { + args := parseArgs(argc, argv) + errorWriter := setupLogging(args) + handle, err := pam.NewHandle(pamh) + + if err == nil { + err = f(handle, args) + } + + if err != nil { + fmt.Fprint(errorWriter, err) + return C.PAM_SERVICE_ERR + } + return C.PAM_SUCCESS +} + +// parseArgs takes a list of C arguments into a PAM function and returns a map +// where a key has a value of true if it appears in the argument list. +func parseArgs(argc C.int, argv **C.char) map[string]bool { + args := make(map[string]bool) + for _, cString := range util.PointerSlice(unsafe.Pointer(argv))[:argc] { + args[C.GoString((*C.char)(cString))] = true + } + return args +} + +// setupLogging directs turns off standard logging (or redirects it to debug +// syslog if the "debug" argument is passed) and returns a writer to the error +// syslog. +func setupLogging(args map[string]bool) io.Writer { + log.SetFlags(0) // Syslog already includes time data itself + log.SetOutput(ioutil.Discard) + if args[debugFlag] { + debugWriter, err := syslog.New(syslog.LOG_DEBUG, moduleName) + if err == nil { + log.SetOutput(debugWriter) + } + } + + errorWriter, err := syslog.New(syslog.LOG_ERR, moduleName) + if err != nil { + return ioutil.Discard + } + return errorWriter +} + +// loginProtector returns the login protector corresponding to the PAM_USER if +// one exists. This protector descriptor (if found) will be cached in the pam +// data, under descriptorLabel. +func loginProtector(handle *pam.Handle) (*actions.Protector, error) { + ctx, err := actions.NewContextFromMountpoint("/") + if err != nil { + return nil, err + } + + // Find the user's PAM protector. + options, err := ctx.ProtectorOptions() + if err != nil { + return nil, err + } + for _, option := range options { + if option.Source() == metadata.SourceType_pam_passphrase && + option.UID() == int64(handle.UID) { + return actions.GetProtectorFromOption(ctx, option) + } + } + return nil, errors.Errorf("no PAM protector for UID=%d on %q", handle.UID, ctx.Mount.Path) +} + +// policiesUsingProtector searches all the mountpoints for any policies +// protected with the specified protector. +func policiesUsingProtector(protector *actions.Protector) []*actions.Policy { + mounts, err := filesystem.AllFilesystems() + if err != nil { + log.Print(err) + return nil + } + + var policies []*actions.Policy + for _, mount := range mounts { + // Skip mountpoints that do not use the protector. + if _, _, err := mount.GetProtector(protector.Descriptor()); err != nil { + continue + } + policyDescriptors, err := mount.ListPolicies() + if err != nil { + log.Printf("listing policies: %s", err) + continue + } + + ctx := &actions.Context{Config: protector.Context.Config, Mount: mount} + for _, policyDescriptor := range policyDescriptors { + policy, err := actions.GetPolicy(ctx, policyDescriptor) + if err != nil { + log.Printf("reading policy: %s", err) + continue + } + + if policy.UsesProtector(protector) { + policies = append(policies, policy) + } + } + } + return policies +} + +// AdjustCount changes the session count for the pam user by the specified +// amount. If the count file does not exist, create it as if it had a count of +// zero. If the adjustment would be the count below zero, the count is set to +// zero. The value of the new count is returned. Requires root privileges. +func AdjustCount(handle *pam.Handle, delta int) (int, error) { + // Make sure the directory exists + if err := os.MkdirAll(countDirectory, countDirectoryPermissions); err != nil { + return 0, err + } + + path := filepath.Join(countDirectory, fmt.Sprintf("%d.count", handle.UID)) + file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, countFilePermissions) + if err != nil { + return 0, err + } + if err := unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil { + return 0, err + } + defer file.Close() + + newCount := util.MaxInt(getCount(file)+delta, 0) + if _, err = file.Seek(0, io.SeekStart); err != nil { + return 0, err + } + if _, err = fmt.Fprintf(file, countFileFormat, newCount); err != nil { + return 0, err + } + + log.Printf("Session count for UID=%d updated to %d", handle.UID, newCount) + return newCount, nil +} + +// Returns the count in the file (or zero if the count cannot be read). +func getCount(file *os.File) int { + var count int + if _, err := fmt.Fscanf(file, countFileFormat, &count); err != nil { + return 0 + } + return count +} diff --git a/security/cache.go b/security/cache.go new file mode 100644 index 0000000..7002014 --- /dev/null +++ b/security/cache.go @@ -0,0 +1,41 @@ +/* + * cache.go - Handles cache clearing and management. + * + * 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 ( + "log" + "os" +) + +// 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 +} diff --git a/security/keyring.go b/security/keyring.go index f75b189..ef56364 100644 --- a/security/keyring.go +++ b/security/keyring.go @@ -141,6 +141,13 @@ func getUserKeyringID() (int, error) { } keyringID := int(parsedID) + // For some stupid reason, a thread does not automaticaly "possess" keys + // in the user keyring. So we link it into the process keyring so that + // we will not get "permission denied" when purging or modifying keys. + if err := keyringLink(keyringID, unix.KEY_SPEC_PROCESS_KEYRING); err != nil { + return 0, err + } + keyringIDCache[euid] = keyringID return keyringID, nil } @@ -151,11 +158,19 @@ func getUserKeyringID() (int, error) { func keyringLink(keyID int, keyringID int) error { _, err := unix.KeyctlInt(unix.KEYCTL_LINK, keyID, keyringID, 0, 0) log.Printf("KeyctlLink(%d, %d) = %v", keyID, keyringID, err) - return errors.Wrap(ErrKeyringLink, err.Error()) + + if err != nil { + return errors.Wrap(ErrKeyringLink, err.Error()) + } + return err } func keyringUnlink(keyID int, keyringID int) error { _, err := unix.KeyctlInt(unix.KEYCTL_UNLINK, keyID, keyringID, 0, 0) log.Printf("KeyctlUnlink(%d, %d) = %v", keyID, keyringID, err) - return errors.Wrap(ErrKeyringUnlink, err.Error()) + + if err != nil { + return errors.Wrap(ErrKeyringUnlink, err.Error()) + } + return err } diff --git a/security/privileges.go b/security/privileges.go index f6e8098..aff41a7 100644 --- a/security/privileges.go +++ b/security/privileges.go @@ -18,6 +18,7 @@ */ // Package security manages: +// - Cache clearing (cache.go) // - Keyring Operations (keyring.go) // - Privilege manipulation (privileges.go) // - Maintaining the link between the root and user keyrings. diff --git a/util/util.go b/util/util.go index acdc3fc..c02ea0e 100644 --- a/util/util.go +++ b/util/util.go @@ -25,7 +25,6 @@ package util import ( "bufio" - "log" "math" "os" "unsafe" @@ -83,6 +82,14 @@ func MinInt(a, b int) int { return b } +// MaxInt returns the greater of a and b. +func MaxInt(a, b int) int { + if a > b { + return a + } + return b +} + // MinInt64 returns the lesser of a and b. func MinInt64(a, b int64) int64 { if a < b { @@ -98,19 +105,3 @@ 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 -} |