]> git.apps.os.sepia.ceph.com Git - fscrypt.git/commitdiff
pam_fscrypt: The actual PAM module and config
authorJoe Richey joerichey@google.com <joerichey@google.com>
Wed, 19 Jul 2017 22:41:23 +0000 (15:41 -0700)
committerJoe Richey joerichey@google.com <joerichey@google.com>
Tue, 22 Aug 2017 18:51:31 +0000 (11:51 -0700)
pam_fscrypt/config [new file with mode: 0644]
pam_fscrypt/pam_fscrypt.go [new file with mode: 0644]

diff --git a/pam_fscrypt/config b/pam_fscrypt/config
new file mode 100644 (file)
index 0000000..26b7767
--- /dev/null
@@ -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
+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 (file)
index 0000000..09e9664
--- /dev/null
@@ -0,0 +1,367 @@
+/*
+ * 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 (
+       "fmt"
+       "io"
+       "io/ioutil"
+       "log"
+       "log/syslog"
+       "os"
+       "unsafe"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/pam"
+       "github.com/google/fscrypt/util"
+)
+
+const (
+       // These labels are used to tag items in the PAM data.
+       authtokLabel         = "fscrypt_authtok"
+       descriptorLabel      = "fscrypt_descriptor"
+       provisionedKeysLabel = "fscrypt_provisioned_keys"
+       moduleName           = "pam_fscrypt"
+)
+
+// 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["debug"] {
+               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
+       }
+
+       // Retrieve the cached value if one exists.
+       if descriptor, err := handle.GetString(descriptorLabel); err == nil {
+               log.Printf("using cached descriptor %q", descriptor)
+               return actions.GetProtector(ctx, descriptor)
+       }
+
+       // Find the user's PAM protector.
+       pamUID, err := handle.GetUID()
+       if err != nil {
+               return nil, err
+       }
+       options, err := ctx.ProtectorOptions()
+       if err != nil {
+               return nil, err
+       }
+       for _, option := range options {
+               if option.Source() != metadata.SourceType_pam_passphrase || option.UID() != pamUID {
+                       continue
+               }
+
+               log.Printf("caching descriptor %q", option.Descriptor())
+               if err = handle.SetString(descriptorLabel, option.Descriptor()); err != nil {
+                       log.Printf("could not set descriptor data: %s", err)
+                       // We can still get the protector, so no error.
+               }
+
+               return actions.GetProtectorFromOption(ctx, option)
+       }
+       return nil, fmt.Errorf("no PAM protector on %q", ctx.Mount.Path)
+}
+
+// pam_sm_authenticate copies the AUTHTOK (if necessary) into the PAM data so it
+// can be used in pam_sm_open_session.
+//export pam_sm_authenticate
+func pam_sm_authenticate(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       handle := pam.NewHandle(pamh)
+       errWriter := setupLogging(parseArgs(argc, argv))
+
+       // 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 C.PAM_SUCCESS
+       }
+
+       log.Print("copying AUTHTOK in pam_sm_authenticate()")
+       authtok, err := handle.GetItem(pam.Authtok)
+       if err != nil {
+               fmt.Fprintf(errWriter, "could not get AUTHTOK: %s", err)
+               return C.PAM_SERVICE_ERR
+       }
+       if err = handle.SetSecret(authtokLabel, authtok); err != nil {
+               fmt.Fprintf(errWriter, "could not set AUTHTOK data: %s", err)
+               return C.PAM_SERVICE_ERR
+       }
+       return C.PAM_SUCCESS
+}
+
+// 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
+}
+
+// policiesToProvision searches all the mountpoints for any unprovisioned
+// policies protected with the specified protector. An error during this search
+// does not halt the search, instead the errors are written to errWriter.
+func policiesToProvision(protector *actions.Protector, errWriter io.Writer) []*actions.Policy {
+       mounts, err := filesystem.AllFilesystems()
+       if err != nil {
+               fmt.Fprint(errWriter, 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 {
+                       fmt.Fprintf(errWriter, "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 {
+                               fmt.Fprintf(errWriter, "reading policy: %s", err)
+                               continue
+                       }
+
+                       if policy.UsesProtector(protector) && !policy.IsProvisioned() {
+                               policies = append(policies, policy)
+                       }
+               }
+       }
+       return policies
+}
+
+// pam_sm_open_session provisions policies protected with the login protector.
+//export pam_sm_open_session
+func pam_sm_open_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       handle := pam.NewHandle(pamh)
+       errWriter := setupLogging(parseArgs(argc, argv))
+
+       protector, err := loginProtector(handle)
+       if err != nil {
+               log.Printf("no directories to unlock: %s", err)
+               return C.PAM_SUCCESS
+       }
+
+       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")
+               }
+               defer handle.ClearData(authtokLabel)
+               return crypto.NewKeyFromCString(authtok)
+       }
+
+       log.Print("searching for policies to provision in pam_sm_open_session()")
+       policies := policiesToProvision(protector, errWriter)
+
+       if len(policies) == 0 {
+               log.Print("no policies to provision")
+               return C.PAM_SUCCESS
+       }
+
+       if err := protector.Unlock(keyFn); err != nil {
+               fmt.Fprintf(errWriter, "unlocking protector %s: %s", protector.Descriptor(), err)
+               return C.PAM_SERVICE_ERR
+       }
+       defer protector.Lock()
+
+       var provisionedKeys []string
+       for _, policy := range policies {
+               if err := policy.UnlockWithProtector(protector); err != nil {
+                       fmt.Fprintf(errWriter, "unlocking policy %s: %s", policy.Descriptor(), err)
+                       continue
+               }
+               defer policy.Lock()
+
+               if err := policy.Provision(); err != nil {
+                       fmt.Fprintf(errWriter, "provisioning policy %s: %s", policy.Descriptor(), err)
+                       continue
+               }
+
+               log.Printf("policy %s provisioned", policy.Descriptor())
+               provisionedKeys = append(provisionedKeys, policy.Description())
+       }
+
+       if len(provisionedKeys) == 0 {
+               fmt.Fprint(errWriter, "could not provision any policies")
+               return C.PAM_SERVICE_ERR
+       }
+
+       if err := handle.SetSlice(provisionedKeysLabel, provisionedKeys); err != nil {
+               fmt.Fprintf(errWriter, "setting key list data: %s", err)
+               return C.PAM_SERVICE_ERR
+       }
+       return C.PAM_SUCCESS
+}
+
+// pam_sm_close_session deprovisions all keys provisioned at the start of the
+// session. It also clears the cache so these changes take effect.
+//export pam_sm_close_session
+func pam_sm_close_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       handle := pam.NewHandle(pamh)
+       args := parseArgs(argc, argv)
+       errWriter := setupLogging(args)
+
+       provisionedKeys, err := handle.GetSlice(provisionedKeysLabel)
+       if err != nil {
+               log.Printf("no directories to lock: %s", err)
+               return C.PAM_SUCCESS
+       }
+
+       log.Print("locking directories in pam_sm_close_session()")
+       for _, provisionedKey := range provisionedKeys {
+               if err := crypto.RemovePolicyKey(provisionedKey); err != nil {
+                       fmt.Fprintf(errWriter, "can't remove %s: %s", provisionedKey, err)
+               }
+       }
+
+       if args["drop_caches"] {
+               log.Print("dropping page caches")
+               // See: https://www.kernel.org/doc/Documentation/sysctl/vm.txt
+               f, err := os.OpenFile("/proc/sys/vm/drop_caches", os.O_WRONLY|os.O_SYNC, 0)
+               if err != nil {
+                       fmt.Fprint(errWriter, err)
+                       return C.PAM_SERVICE_ERR
+               }
+               defer f.Close()
+               // "3" clears slab objects and the page cache
+               if _, err := f.WriteString("3"); err != nil {
+                       fmt.Fprint(errWriter, err)
+                       return C.PAM_SERVICE_ERR
+               }
+       }
+
+       return C.PAM_SUCCESS
+}
+
+// pam_sm_chauthtok rewraps the login protector when the passphrase changes.
+//export pam_sm_chauthtok
+func pam_sm_chauthtok(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       handle := pam.NewHandle(pamh)
+       errWriter := setupLogging(parseArgs(argc, argv))
+
+       // Only do rewrapping if we have both AUTHTOKs and a login protector.
+       if pam.Flag(flags)&pam.PrelimCheck != 0 {
+               log.Print("no preliminary checks need to run")
+               return C.PAM_SUCCESS
+       }
+       protector, err := loginProtector(handle)
+       if err != nil {
+               log.Printf("no protector to rewrap: %s", err)
+               return C.PAM_SUCCESS
+       }
+
+       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("rewrapping protector in pam_sm_chauthtok()")
+       if err = protector.Unlock(oldKeyFn); err != nil {
+               fmt.Fprint(errWriter, err)
+               return C.PAM_SERVICE_ERR
+       }
+       defer protector.Lock()
+       if err = protector.Rewrap(newKeyFn); err != nil {
+               fmt.Fprint(errWriter, err)
+               return C.PAM_SERVICE_ERR
+       }
+
+       return C.PAM_SUCCESS
+}
+
+// main() is needed to make a shared library compile
+func main() {}