aboutsummaryrefslogtreecommitdiff
path: root/pam_fscrypt/pam_fscrypt.go
diff options
context:
space:
mode:
Diffstat (limited to 'pam_fscrypt/pam_fscrypt.go')
-rw-r--r--pam_fscrypt/pam_fscrypt.go236
1 files changed, 188 insertions, 48 deletions
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