aboutsummaryrefslogtreecommitdiff
path: root/pam_fscrypt
diff options
context:
space:
mode:
Diffstat (limited to 'pam_fscrypt')
-rw-r--r--pam_fscrypt/config8
-rw-r--r--pam_fscrypt/pam_fscrypt.go236
-rw-r--r--pam_fscrypt/run_fscrypt.go94
-rw-r--r--pam_fscrypt/run_test.go2
4 files changed, 266 insertions, 74 deletions
diff --git a/pam_fscrypt/config b/pam_fscrypt/config
index 795a4f8..f83dab2 100644
--- a/pam_fscrypt/config
+++ b/pam_fscrypt/config
@@ -1,13 +1,13 @@
Name: fscrypt PAM passphrase support
Default: yes
-Priority: 0
+Priority: 100
Auth-Type: Additional
Auth-Final:
- optional pam_fscrypt.so
+ optional PAM_INSTALL_PATH
Session-Type: Additional
Session-Interactive-Only: yes
Session-Final:
- optional pam_fscrypt.so drop_caches lock_policies
+ optional PAM_INSTALL_PATH
Password-Type: Additional
Password-Final:
- optional pam_fscrypt.so
+ optional PAM_INSTALL_PATH
diff --git a/pam_fscrypt/pam_fscrypt.go b/pam_fscrypt/pam_fscrypt.go
index 571a42b..15066c1 100644
--- a/pam_fscrypt/pam_fscrypt.go
+++ b/pam_fscrypt/pam_fscrypt.go
@@ -17,8 +17,6 @@
* the License.
*/
-// +build linux,cgo
-
package main
/*
@@ -31,13 +29,18 @@ package main
*/
import "C"
import (
+ "fmt"
"log"
+ "log/syslog"
+ "os"
+ "strconv"
"unsafe"
"github.com/pkg/errors"
"github.com/google/fscrypt/actions"
"github.com/google/fscrypt/crypto"
+ "github.com/google/fscrypt/keyring"
"github.com/google/fscrypt/pam"
"github.com/google/fscrypt/security"
)
@@ -46,21 +49,47 @@ const (
moduleName = "pam_fscrypt"
// authtokLabel tags the AUTHTOK in the PAM data.
authtokLabel = "fscrypt_authtok"
+ // pidLabel tags the pid in the PAM data.
+ pidLabel = "fscrypt_pid"
// These flags are used to toggle behavior of the PAM module.
debugFlag = "debug"
- lockFlag = "lock_policies"
- cacheFlag = "drop_caches"
+
+ // This option is accepted for compatibility with existing config files,
+ // but now we lock policies by default and this option is a no-op.
+ lockPoliciesFlag = "lock_policies"
+
+ // Only unlock directories, don't lock them.
+ unlockOnlyFlag = "unlock_only"
+
+ // This option is accepted for compatibility with existing config files,
+ // but it no longer does anything. pam_fscrypt now drops caches if and
+ // only if it is needed. (Usually it is not needed anymore, as the
+ // FS_IOC_REMOVE_ENCRYPTION_KEY ioctl handles this automatically.)
+ dropCachesFlag = "drop_caches"
+)
+
+var (
+ // PamFuncs for our 4 provided methods
+ authenticateFunc = PamFunc{"Authenticate", Authenticate}
+ openSessionFunc = PamFunc{"OpenSession", OpenSession}
+ closeSessionFunc = PamFunc{"CloseSession", CloseSession}
+ chauthtokFunc = PamFunc{"Chauthtok", Chauthtok}
)
// 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 {
- log.Print("Authenticate()")
if err := handle.StartAsPamUser(); err != nil {
return err
}
defer handle.StopAsPamUser()
+ // Save the PID in the PAM data so that the Session hook can try to
+ // detect the unsupported situation where the process was forked.
+ if err := handle.SetString(pidLabel, strconv.Itoa(os.Getpid())); err != nil {
+ return errors.Wrap(err, "could not save pid in PAM data")
+ }
+
// If this user doesn't have a login protector, no unlocking is needed.
if _, err := loginProtector(handle); err != nil {
log.Printf("no protector, no need for AUTHTOK: %s", err)
@@ -76,10 +105,77 @@ func Authenticate(handle *pam.Handle, _ map[string]bool) error {
return errors.Wrap(err, "could not set AUTHTOK data")
}
+func beginProvisioningOp(handle *pam.Handle, policy *actions.Policy) error {
+ if policy.NeedsRootToProvision() {
+ return handle.StopAsPamUser()
+ }
+ return nil
+}
+
+func endProvisioningOp(handle *pam.Handle, policy *actions.Policy) error {
+ if policy.NeedsRootToProvision() {
+ return handle.StartAsPamUser()
+ }
+ return nil
+}
+
+// Set up the PAM user's keyring if needed by any encryption policies.
+func setupUserKeyringIfNeeded(handle *pam.Handle, policies []*actions.Policy) error {
+ needed := false
+ for _, policy := range policies {
+ if policy.NeedsUserKeyring() {
+ needed = true
+ break
+ }
+ }
+ if !needed {
+ return nil
+ }
+ err := handle.StopAsPamUser()
+ if err != nil {
+ return err
+ }
+ _, err = keyring.UserKeyringID(handle.PamUser, true)
+ if err != nil {
+ log.Printf("Setting up keyrings in PAM: %v", err)
+ }
+ return handle.StartAsPamUser()
+}
+
+// The Go runtime doesn't support being forked, as it is multithreaded but
+// fork() deletes all threads except one. Some programs, such as xrdp, misuse
+// libpam by fork()-ing the process between pam_authenticate() and
+// pam_open_session(). Try to detect such unsupported cases and bail out early
+// rather than deadlocking the Go runtime, which would prevent the user from
+// logging in entirely. This isn't guaranteed to work, as we are already
+// running Go code here, so we may have already deadlocked. But in practice the
+// deadlock doesn't occur until hashing the login passphrase is attempted.
+func isUnsupportedFork(handle *pam.Handle) bool {
+ pidString, err := handle.GetString(pidLabel)
+ if err != nil {
+ return false
+ }
+ expectedPid, err := strconv.Atoi(pidString)
+ if err != nil {
+ log.Printf("%s parse error: %v", pidLabel, err)
+ return false
+ }
+ if os.Getpid() == expectedPid {
+ return false
+ }
+ handle.InfoMessage(fmt.Sprintf("%s couldn't automatically unlock directories, see syslog", moduleName))
+ if logger, err := syslog.New(syslog.LOG_WARNING, moduleName); err == nil {
+ fmt.Fprintf(logger,
+ "not unlocking directories because %s forked the process between authenticating the user and opening the session, which is incompatible with %s. See https://github.com/google/fscrypt/issues/350",
+ handle.GetServiceName(), moduleName)
+ logger.Close()
+ }
+ return true
+}
+
// OpenSession provisions any policies protected with the login protector.
func OpenSession(handle *pam.Handle, _ map[string]bool) error {
- log.Print("OpenSession()")
- // We will always clear the the AUTHTOK data
+ // We will always clear the AUTHTOK data
defer handle.ClearData(authtokLabel)
// Increment the count as we add a session
if _, err := AdjustCount(handle, +1); err != nil {
@@ -97,12 +193,20 @@ func OpenSession(handle *pam.Handle, _ map[string]bool) error {
log.Printf("no protector to unlock: %s", err)
return nil
}
- policies := policiesUsingProtector(protector)
+ policies := policiesUsingProtector(protector, false)
if len(policies) == 0 {
log.Print("no policies to unlock")
return nil
}
+ if isUnsupportedFork(handle) {
+ return nil
+ }
+
+ if err = setupUserKeyringIfNeeded(handle, policies); err != nil {
+ return errors.Wrapf(err, "setting up user keyring")
+ }
+
log.Printf("unlocking %d policies protected with AUTHTOK", len(policies))
keyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) {
if retry {
@@ -130,21 +234,25 @@ func OpenSession(handle *pam.Handle, _ map[string]bool) error {
// 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)
+ if err := beginProvisioningOp(handle, policy); err != nil {
+ return err
+ }
+ provisionErr := policy.Provision()
+ if err := endProvisioningOp(handle, policy); err != nil {
+ return err
+ }
+ if provisionErr != nil {
+ log.Printf("provisioning policy %s: %s", policy.Descriptor(), provisionErr)
continue
}
- log.Printf("policy %s provisioned", policy.Descriptor())
+ log.Printf("policy %s provisioned by %v", policy.Descriptor(),
+ handle.PamUser.Username)
}
return nil
}
@@ -152,36 +260,48 @@ func OpenSession(handle *pam.Handle, _ map[string]bool) error {
// 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 {
- log.Printf("CloseSession(%v)", args)
// 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("locking polices protected with login protector")
- errLock = lockLoginPolicies(handle)
+ if args[lockPoliciesFlag] {
+ log.Print("ignoring deprecated 'lock_policies' option (now the default)")
}
- if args[cacheFlag] {
- log.Print("dropping appropriate filesystem caches at session close")
- errCache = security.DropFilesystemCache()
+ if args[dropCachesFlag] {
+ log.Print("ignoring deprecated 'drop_caches' option (now auto-detected)")
}
- if errLock != nil {
- return errLock
+ // Don't automatically drop privileges, since we may need them to
+ // deprovision policies or to drop caches.
+
+ if !args[unlockOnlyFlag] {
+ log.Print("locking policies protected with login protector")
+ needDropCaches, errLock := lockLoginPolicies(handle)
+
+ var errCache error
+ if needDropCaches {
+ log.Print("dropping appropriate filesystem caches at session close")
+ errCache = security.DropFilesystemCache()
+ }
+ if errLock != nil {
+ return errLock
+ }
+ return errCache
}
- return errCache
+ return nil
}
-// lockLoginPolicies deprovisions all policy keys that are protected by
-// the user's login protector.
-func lockLoginPolicies(handle *pam.Handle) error {
+// lockLoginPolicies deprovisions all policy keys that are protected by the
+// user's login protector. It returns true if dropping filesystem caches will
+// be needed afterwards to completely lock the relevant directories.
+func lockLoginPolicies(handle *pam.Handle) (bool, error) {
+ needDropCaches := false
+
if err := handle.StartAsPamUser(); err != nil {
- return err
+ return needDropCaches, err
}
defer handle.StopAsPamUser()
@@ -189,32 +309,49 @@ func lockLoginPolicies(handle *pam.Handle) error {
protector, err := loginProtector(handle)
if err != nil {
log.Printf("nothing to lock: %s", err)
- return nil
+ return needDropCaches, nil
}
- policies := policiesUsingProtector(protector)
+ policies := policiesUsingProtector(protector, true)
if len(policies) == 0 {
log.Print("no policies to lock")
- return nil
+ return needDropCaches, nil
+ }
+
+ if err = setupUserKeyringIfNeeded(handle, policies); err != nil {
+ return needDropCaches, errors.Wrapf(err, "setting up user keyring")
}
// 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 policy.NeedsUserKeyring() {
+ needDropCaches = true
+ }
+ if err := beginProvisioningOp(handle, policy); err != nil {
+ return needDropCaches, err
}
- if err := policy.Deprovision(); err != nil {
- log.Printf("deprovisioning policy %s: %s", policy.Descriptor(), err)
+ deprovisionErr := policy.Deprovision(false)
+ if err := endProvisioningOp(handle, policy); err != nil {
+ return needDropCaches, err
+ }
+ if deprovisionErr != nil {
+ log.Printf("deprovisioning policy %s: %s", policy.Descriptor(), deprovisionErr)
continue
}
- log.Printf("policy %s deprovisioned", policy.Descriptor())
+ log.Printf("policy %s deprovisioned by %v", policy.Descriptor(), handle.PamUser.Username)
}
- return nil
+ return needDropCaches, nil
}
+var noOldAuthTokMessage string = `
+pam_fscrypt: cannot update login protector for '%s' because old passphrase
+was not given. This is expected when changing a user's passphrase as root.
+You'll need to manually update the protector's passphrase using:
+
+ fscrypt metadata change-passphrase --protector=%s:%s
+`
+
// Chauthtok rewraps the login protector when the passphrase changes.
func Chauthtok(handle *pam.Handle, _ map[string]bool) error {
- log.Print("Chauthtok()")
if err := handle.StartAsPamUser(); err != nil {
return err
}
@@ -235,6 +372,9 @@ func Chauthtok(handle *pam.Handle, _ map[string]bool) error {
}
authtok, err := handle.GetItem(pam.Oldauthtok)
if err != nil {
+ handle.InfoMessage(fmt.Sprintf(noOldAuthTokMessage,
+ handle.PamUser.Username,
+ protector.Context.Mount.Path, protector.Descriptor()))
return nil, errors.Wrap(err, "could not get OLDAUTHTOK")
}
return crypto.NewKeyFromCString(authtok)
@@ -259,10 +399,11 @@ func Chauthtok(handle *pam.Handle, _ map[string]bool) error {
//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)
+ return authenticateFunc.Run(pamh, argc, argv)
}
-// pam_sm_stecred needed because we use pam_sm_authenticate.
+// pam_sm_setcred 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
@@ -270,12 +411,12 @@ func pam_sm_setcred(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int
//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)
+ return openSessionFunc.Run(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)
+ return closeSessionFunc.Run(pamh, argc, argv)
}
//export pam_sm_chauthtok
@@ -284,8 +425,7 @@ func pam_sm_chauthtok(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.i
if pam.Flag(flags)&pam.PrelimCheck != 0 {
return C.PAM_SUCCESS
}
-
- return RunPamFunc(Chauthtok, pamh, argc, argv)
+ return chauthtokFunc.Run(pamh, argc, argv)
}
// main() is needed to make a shared library compile
diff --git a/pam_fscrypt/run_fscrypt.go b/pam_fscrypt/run_fscrypt.go
index 6414d99..af9537f 100644
--- a/pam_fscrypt/run_fscrypt.go
+++ b/pam_fscrypt/run_fscrypt.go
@@ -31,10 +31,10 @@ import "C"
import (
"fmt"
"io"
- "io/ioutil"
"log"
"log/syslog"
"os"
+ "os/user"
"path/filepath"
"runtime/debug"
"unsafe"
@@ -57,13 +57,30 @@ const (
countDirectoryPermissions = 0700
countFilePermissions = 0600
countFileFormat = "%d\n"
+ // uidMin is the first UID that can be used for a regular user (as
+ // opposed to a system user or root). This value is fairly standard
+ // across Linux distros, but it can be adjusted if needed.
+ uidMin = 1000
)
-// PamFunc is used to define the various actions in the PAM module
-type PamFunc func(handle *pam.Handle, args map[string]bool) error
+// PamFunc is used to define the various actions in the PAM module.
+type PamFunc struct {
+ // Name of the function being executed
+ name string
+ // Go implementation of this function
+ impl func(handle *pam.Handle, args map[string]bool) error
+}
+
+// isSystemUser checks if a user is a system user. pam_fscrypt should never
+// need to do anything for system users since they should never have login
+// protectors. Therefore, we detect them early to avoid wasting resources.
+func isSystemUser(user *user.User) bool {
+ uid := util.AtoiOrPanic(user.Uid)
+ return uid < uidMin && uid != 0
+}
-// 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) (ret C.int) {
+// Run is used to convert between the Go functions and exported C funcs.
+func (f *PamFunc) Run(pamh unsafe.Pointer, argc C.int, argv **C.char) (ret C.int) {
args := parseArgs(argc, argv)
errorWriter := setupLogging(args)
@@ -72,20 +89,27 @@ func RunPamFunc(f PamFunc, pamh unsafe.Pointer, argc C.int, argv **C.char) (ret
if r := recover(); r != nil {
ret = C.PAM_SERVICE_ERR
fmt.Fprintf(errorWriter,
- "pam func panicked: %s\nPlease open an issue.\n%s",
- r, debug.Stack())
+ "%s(%v) panicked: %s\nPlease open a bug.\n%s",
+ f.name, args, r, debug.Stack())
}
}()
+ log.Printf("%s(%v) starting", f.name, args)
handle, err := pam.NewHandle(pamh)
if err == nil {
- err = f(handle, args)
+ if isSystemUser(handle.PamUser) {
+ log.Printf("invoked for system user %q (%s), doing nothing",
+ handle.PamUser.Username, handle.PamUser.Uid)
+ err = nil
+ } else {
+ err = f.impl(handle, args)
+ }
}
if err != nil {
- fmt.Fprintf(errorWriter, "pam func failed: %s", err)
+ fmt.Fprintf(errorWriter, "%s(%v) failed: %s", f.name, args, err)
return C.PAM_SERVICE_ERR
}
- log.Print("pam func succeeded")
+ log.Printf("%s(%v) succeeded", f.name, args)
return C.PAM_SUCCESS
}
@@ -107,7 +131,7 @@ func parseArgs(argc C.int, argv **C.char) map[string]bool {
// syslog.
func setupLogging(args map[string]bool) io.Writer {
log.SetFlags(0) // Syslog already includes time data itself
- log.SetOutput(ioutil.Discard)
+ log.SetOutput(io.Discard)
if args[debugFlag] {
debugWriter, err := syslog.New(syslog.LOG_DEBUG, moduleName)
if err == nil {
@@ -117,7 +141,7 @@ func setupLogging(args map[string]bool) io.Writer {
errorWriter, err := syslog.New(syslog.LOG_ERR, moduleName)
if err != nil {
- return ioutil.Discard
+ return io.Discard
}
return errorWriter
}
@@ -126,10 +150,18 @@ func setupLogging(args map[string]bool) io.Writer {
// 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("/", handle.PamUser)
+ ctx, err := actions.NewContextFromMountpoint(actions.LoginProtectorMountpoint,
+ handle.PamUser)
if err != nil {
return nil, err
}
+ // Ensure that pam_fscrypt only processes metadata files owned by the
+ // user or root, even if the user is root themselves. (Normally, when
+ // fscrypt is run as root it is allowed to process all metadata files.
+ // This implements stricter behavior for pam_fscrypt.)
+ if !ctx.Config.GetAllowCrossUserMetadata() {
+ ctx.TrustedUser = handle.PamUser
+ }
// Find the user's PAM protector.
options, err := ctx.ProtectorOptions()
@@ -146,8 +178,10 @@ func loginProtector(handle *pam.Handle) (*actions.Protector, error) {
}
// policiesUsingProtector searches all the mountpoints for any policies
-// protected with the specified protector.
-func policiesUsingProtector(protector *actions.Protector) []*actions.Policy {
+// protected with the specified protector. If provisioned is true, then only
+// policies provisioned by the target user are returned; otherwise only policies
+// *not* provisioned by the target user are returned.
+func policiesUsingProtector(protector *actions.Protector, provisioned bool) []*actions.Policy {
mounts, err := filesystem.AllFilesystems()
if err != nil {
log.Print(err)
@@ -157,10 +191,14 @@ func policiesUsingProtector(protector *actions.Protector) []*actions.Policy {
var policies []*actions.Policy
for _, mount := range mounts {
// Skip mountpoints that do not use the protector.
- if _, _, err := mount.GetProtector(protector.Descriptor()); err != nil {
+ if _, _, err := mount.GetProtector(protector.Descriptor(),
+ protector.Context.TrustedUser); err != nil {
+ if _, ok := err.(*filesystem.ErrNotSetup); !ok {
+ log.Print(err)
+ }
continue
}
- policyDescriptors, err := mount.ListPolicies()
+ policyDescriptors, err := mount.ListPolicies(protector.Context.TrustedUser)
if err != nil {
log.Printf("listing policies: %s", err)
continue
@@ -176,9 +214,23 @@ func policiesUsingProtector(protector *actions.Protector) []*actions.Policy {
continue
}
- if policy.UsesProtector(protector) {
- policies = append(policies, policy)
+ if !policy.UsesProtector(protector) {
+ continue
+ }
+ if provisioned {
+ if !policy.IsProvisionedByTargetUser() {
+ log.Printf("policy %s not provisioned by %v",
+ policy.Descriptor(), ctx.TargetUser.Username)
+ continue
+ }
+ } else {
+ if policy.IsProvisionedByTargetUser() {
+ log.Printf("policy %s already provisioned by %v",
+ policy.Descriptor(), ctx.TargetUser.Username)
+ continue
+ }
}
+ policies = append(policies, policy)
}
}
return policies
@@ -186,7 +238,7 @@ func policiesUsingProtector(protector *actions.Protector) []*actions.Policy {
// 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. If the adjustment would bring 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
@@ -199,7 +251,7 @@ func AdjustCount(handle *pam.Handle, delta int) (int, error) {
if err != nil {
return 0, err
}
- if err := unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil {
+ if err = unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil {
return 0, err
}
defer file.Close()
diff --git a/pam_fscrypt/run_test.go b/pam_fscrypt/run_test.go
index 1e74528..40ace4c 100644
--- a/pam_fscrypt/run_test.go
+++ b/pam_fscrypt/run_test.go
@@ -1,5 +1,5 @@
/*
- * run_test.go - tests that the PAM helper functionsd work properly
+ * run_test.go - tests that the PAM helper functions work properly
*
* Copyright 2017 Google Inc.
* Author: Joe Richey (joerichey@google.com)