aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Richey <joerichey@google.com>2017-08-24 00:53:11 -0700
committerGitHub <noreply@github.com>2017-08-24 00:53:11 -0700
commit4879df9a6063886865b94c270660838060acbc20 (patch)
tree9adaa99808990c0034484ed24d587c07ac70525d
parent17794e94ebe140dc74f93abb8132f5295ee2004e (diff)
parent19c13e861996c3503be5b0dc5a2cecfe186b1744 (diff)
Merge pull request #25 from google/fixv0.2.00.2.0
fscrypt PAM module
-rw-r--r--.gitignore4
-rw-r--r--CONTRIBUTING.md17
-rw-r--r--Makefile34
-rw-r--r--README.md80
-rw-r--r--actions/policy.go8
-rw-r--r--cmd/fscrypt/commands.go4
-rw-r--r--crypto/key.go1
-rw-r--r--pam/login.go4
-rw-r--r--pam/pam.go84
-rw-r--r--pam_fscrypt/config13
-rw-r--r--pam_fscrypt/pam_fscrypt.go286
-rw-r--r--pam_fscrypt/run_fscrypt.go210
-rw-r--r--security/cache.go41
-rw-r--r--security/keyring.go19
-rw-r--r--security/privileges.go1
-rw-r--r--util/util.go25
16 files changed, 734 insertions, 97 deletions
diff --git a/.gitignore b/.gitignore
index 2491d40..34880d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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:
diff --git a/Makefile b/Makefile
index 4ebbe32..c5b43ce 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
index e2df8ca..2214dad 100644
--- a/README.md
+++ b/README.md
@@ -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
}
diff --git a/pam/pam.go b/pam/pam.go
index 9188b6e..12f2e97 100644
--- a/pam/pam.go
+++ b/pam/pam.go
@@ -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
-}