diff options
Diffstat (limited to 'pam_fscrypt/pam_fscrypt.go')
| -rw-r--r-- | pam_fscrypt/pam_fscrypt.go | 236 |
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 |