aboutsummaryrefslogtreecommitdiff
path: root/actions
diff options
context:
space:
mode:
Diffstat (limited to 'actions')
-rw-r--r--actions/config.go159
-rw-r--r--actions/config_test.go78
-rw-r--r--actions/context.go110
-rw-r--r--actions/context_test.go8
-rw-r--r--actions/hashing_test.go8
-rw-r--r--actions/policy.go318
-rw-r--r--actions/policy_test.go14
-rw-r--r--actions/protector.go90
-rw-r--r--actions/protector_test.go4
-rw-r--r--actions/recovery.go134
-rw-r--r--actions/recovery_test.go90
11 files changed, 818 insertions, 195 deletions
diff --git a/actions/config.go b/actions/config.go
index 81f6e4f..bd4ae28 100644
--- a/actions/config.go
+++ b/actions/config.go
@@ -22,32 +22,76 @@ package actions
import (
"bytes"
+ "fmt"
"log"
+ "math"
"os"
"runtime"
"time"
- "github.com/pkg/errors"
"golang.org/x/sys/unix"
+ "google.golang.org/protobuf/proto"
+ "github.com/google/fscrypt/cgroup"
"github.com/google/fscrypt/crypto"
+ "github.com/google/fscrypt/filesystem"
"github.com/google/fscrypt/metadata"
"github.com/google/fscrypt/util"
)
-// LegacyConfig indicates that keys should be inserted into the keyring with the
-// legacy service prefixes. Needed for kernels before v4.8.
-const LegacyConfig = "legacy"
-
// ConfigFileLocation is the location of fscrypt's global settings. This can be
// overridden by the user of this package.
var ConfigFileLocation = "/etc/fscrypt.conf"
+// ErrBadConfig is an internal error that indicates that the config struct is invalid.
+type ErrBadConfig struct {
+ Config *metadata.Config
+ UnderlyingError error
+}
+
+func (err *ErrBadConfig) Error() string {
+ return fmt.Sprintf(`internal error: config is invalid: %s
+
+ The invalid config is %s`, err.UnderlyingError, err.Config)
+}
+
+// ErrBadConfigFile indicates that the config file is invalid.
+type ErrBadConfigFile struct {
+ Path string
+ UnderlyingError error
+}
+
+func (err *ErrBadConfigFile) Error() string {
+ return fmt.Sprintf("%q is invalid: %s", err.Path, err.UnderlyingError)
+}
+
+// ErrConfigFileExists indicates that the config file already exists.
+type ErrConfigFileExists struct {
+ Path string
+}
+
+func (err *ErrConfigFileExists) Error() string {
+ return fmt.Sprintf("%q already exists", err.Path)
+}
+
+// ErrNoConfigFile indicates that the config file doesn't exist.
+type ErrNoConfigFile struct {
+ Path string
+}
+
+func (err *ErrNoConfigFile) Error() string {
+ return fmt.Sprintf("%q doesn't exist", err.Path)
+}
+
const (
// Permissions of the config file (global readable)
configPermissions = 0644
// Config file should be created for writing and not already exist
createFlags = os.O_CREATE | os.O_WRONLY | os.O_EXCL
+ // 128 MiB is a large enough amount of memory to make the password hash
+ // very difficult to brute force on specialized hardware, but small
+ // enough to work on most GNU/Linux systems.
+ maxMemoryBytes = 128 * 1024 * 1024
)
var (
@@ -56,18 +100,17 @@ var (
)
// CreateConfigFile creates a new config file at the appropriate location with
-// the appropriate hashing costs and encryption parameters. This creation is
-// configurable in two ways. First, a time target must be specified. This target
-// will determine the hashing costs, by picking parameters that make the hashing
-// take as long as the specified target. Second, the config can include the
-// legacy option, which is needed for systems with kernels older than v4.8.
-func CreateConfigFile(target time.Duration, useLegacy bool) error {
+// the appropriate hashing costs and encryption parameters. The hashing will be
+// configured to take as long as the specified time target. In addition, the
+// version of encryption policy to use may be overridden from the default of v1.
+func CreateConfigFile(target time.Duration, policyVersion int64) error {
// Create the config file before computing the hashing costs, so we fail
// immediately if the program has insufficient permissions.
- configFile, err := os.OpenFile(ConfigFileLocation, createFlags, configPermissions)
+ configFile, err := filesystem.OpenFileOverridingUmask(ConfigFileLocation,
+ createFlags, configPermissions)
switch {
case os.IsExist(err):
- return ErrConfigFileExists
+ return &ErrConfigFileExists{ConfigFileLocation}
case err != nil:
return err
}
@@ -77,9 +120,9 @@ func CreateConfigFile(target time.Duration, useLegacy bool) error {
Source: metadata.DefaultSource,
Options: metadata.DefaultOptions,
}
- if useLegacy {
- config.Compatibility = LegacyConfig
- log.Printf("Using %q compatibility option\n", LegacyConfig)
+
+ if policyVersion != 0 {
+ config.Options.PolicyVersion = policyVersion
}
if config.HashCosts, err = getHashingCosts(target); err != nil {
@@ -98,7 +141,7 @@ func getConfig() (*metadata.Config, error) {
configFile, err := os.Open(ConfigFileLocation)
switch {
case os.IsNotExist(err):
- return nil, ErrNoConfigFile
+ return nil, &ErrNoConfigFile{ConfigFileLocation}
case err != nil:
return nil, err
}
@@ -107,7 +150,7 @@ func getConfig() (*metadata.Config, error) {
log.Printf("Reading config from %q\n", ConfigFileLocation)
config, err := metadata.ReadConfig(configFile)
if err != nil {
- return nil, errors.Wrap(ErrBadConfigFile, err.Error())
+ return nil, &ErrBadConfigFile{ConfigFileLocation, err}
}
// Use system defaults if not specified
@@ -127,9 +170,13 @@ func getConfig() (*metadata.Config, error) {
config.Options.Filenames = metadata.DefaultOptions.Filenames
log.Printf("Falling back to filenames mode of %q", config.Options.Filenames)
}
+ if config.Options.PolicyVersion == 0 {
+ config.Options.PolicyVersion = metadata.DefaultOptions.PolicyVersion
+ log.Printf("Falling back to policy version of %d", config.Options.PolicyVersion)
+ }
if err := config.CheckValidity(); err != nil {
- return nil, errors.Wrap(ErrBadConfigFile, err.Error())
+ return nil, &ErrBadConfigFile{ConfigFileLocation, err}
}
return config, nil
@@ -141,12 +188,19 @@ func getConfig() (*metadata.Config, error) {
func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) {
log.Printf("Finding hashing costs that take %v\n", target)
- // Start out with the minimal possible costs that use all the CPUs.
- nCPUs := int64(runtime.NumCPU())
+ // Start out with the minimal possible costs that use all the available
+ // CPUs, respecting cgroup limits when present.
+ parallelism := int64(effectiveCPUCount())
+ // golang.org/x/crypto/argon2 only supports parallelism up to 255.
+ // For compatibility, don't use more than that amount.
+ if parallelism > metadata.MaxParallelism {
+ parallelism = metadata.MaxParallelism
+ }
costs := &metadata.HashingCosts{
- Time: 1,
- Memory: 8 * nCPUs,
- Parallelism: nCPUs,
+ Time: 1,
+ Memory: 8 * parallelism,
+ Parallelism: parallelism,
+ TruncationFixed: true,
}
// If even the minimal costs are not fast enough, just return the
@@ -163,15 +217,15 @@ func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) {
}
// Now we start doubling the costs until we reach the target.
- maxMemory := ramLimit()
+ memoryKiBLimit := memoryBytesLimit() / 1024
for {
// Store a copy of the previous costs
- costsPrev := *costs
+ costsPrev := proto.Clone(costs).(*metadata.HashingCosts)
tPrev := t
- // Double the memory up to the max, then the double the time.
- if costs.Memory < maxMemory {
- costs.Memory = util.MinInt64(2*costs.Memory, maxMemory)
+ // Double the memory up to the max, then double the time.
+ if costs.Memory < memoryKiBLimit {
+ costs.Memory = util.MinInt64(2*costs.Memory, memoryKiBLimit)
} else {
costs.Time *= 2
}
@@ -179,7 +233,7 @@ func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) {
// If our hashing failed, return the last good set of costs.
if t, err = timeHashingCosts(costs); err != nil {
log.Printf("Hashing with costs={%v} failed: %v\n", costs, err)
- return &costsPrev, nil
+ return costsPrev, nil
}
log.Printf("Costs={%v}\t-> %v\n", costs, t)
@@ -188,23 +242,47 @@ func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) {
if t >= target {
f := float64(target-tPrev) / float64(t-tPrev)
return &metadata.HashingCosts{
- Time: betweenCosts(costsPrev.Time, costs.Time, f),
- Memory: betweenCosts(costsPrev.Memory, costs.Memory, f),
- Parallelism: costs.Parallelism,
+ Time: betweenCosts(costsPrev.Time, costs.Time, f),
+ Memory: betweenCosts(costsPrev.Memory, costs.Memory, f),
+ Parallelism: costs.Parallelism,
+ TruncationFixed: costs.TruncationFixed,
}, nil
}
}
}
-// ramLimit returns the maximum amount of RAM (in kB) we will use for passphrase
-// hashing. Right now it is simply half of the total RAM on the system.
-func ramLimit() int64 {
+// effectiveCPUCount returns the number of CPUs available to this process,
+// taking cgroup limits into account. Falls back to runtime.NumCPU() when
+// cgroup information is unavailable.
+func effectiveCPUCount() int {
+ cg, err := cgroup.New()
+ if err != nil {
+ return runtime.NumCPU()
+ }
+ quota, err := cg.CPUQuota()
+ if err != nil || quota <= 0 {
+ return runtime.NumCPU()
+ }
+ cpus := int(math.Ceil(quota))
+ return min(cpus, runtime.NumCPU())
+}
+
+// memoryBytesLimit returns the maximum amount of memory we will use for
+// passphrase hashing. This will never be more than a reasonable maximum (for
+// compatibility) or an 8th the available RAM (considering cgroup limits).
+func memoryBytesLimit() int64 {
+ // The sysinfo syscall only fails if given a bad address
var info unix.Sysinfo_t
err := unix.Sysinfo(&info)
- // The sysinfo syscall only fails if given a bad address
util.NeverError(err)
- // Use half the RAM and convert to kiB.
- return int64(info.Totalram / 1024 / 2)
+
+ totalRAMBytes := int64(info.Totalram)
+ if cg, err := cgroup.New(); err == nil {
+ if cgroupMem, err := cg.MemoryLimit(); err == nil && cgroupMem > 0 {
+ totalRAMBytes = util.MinInt64(totalRAMBytes, cgroupMem)
+ }
+ }
+ return util.MinInt64(totalRAMBytes/8, maxMemoryBytes)
}
// betweenCosts returns a cost between a and b. Specifically, it returns the
@@ -230,6 +308,9 @@ func timeHashingCosts(costs *metadata.HashingCosts) (time.Duration, error) {
}
end := cpuTimeInNanoseconds()
+ // This uses a lot of memory, run the garbage collector
+ runtime.GC()
+
return time.Duration((end - begin) / costs.Parallelism), nil
}
diff --git a/actions/config_test.go b/actions/config_test.go
new file mode 100644
index 0000000..49838e3
--- /dev/null
+++ b/actions/config_test.go
@@ -0,0 +1,78 @@
+/*
+ * config_test.go - tests for creating the config file
+ *
+ * Copyright 2019 Google LLC
+ *
+ * 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 actions
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/google/fscrypt/metadata"
+)
+
+// Test that the global config file is created with mode 0644, regardless of the
+// current umask.
+func TestConfigFileIsCreatedWithCorrectMode(t *testing.T) {
+ oldMask := unix.Umask(0)
+ defer unix.Umask(oldMask)
+ unix.Umask(0077)
+
+ tempDir, err := os.MkdirTemp("", "fscrypt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+ ConfigFileLocation = filepath.Join(tempDir, "test.conf")
+
+ if err = CreateConfigFile(time.Millisecond, 0); err != nil {
+ t.Fatal(err)
+ }
+ fileInfo, err := os.Stat(ConfigFileLocation)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fileInfo.Mode().Perm() != 0644 {
+ t.Error("Expected newly created config file to have mode 0644")
+ }
+}
+
+func TestCreateConfigFileV2Policy(t *testing.T) {
+ tempDir, err := os.MkdirTemp("", "fscrypt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+ ConfigFileLocation = filepath.Join(tempDir, "test.conf")
+
+ if err = CreateConfigFile(time.Millisecond, 2); err != nil {
+ t.Fatal(err)
+ }
+
+ var config *metadata.Config
+ config, err = getConfig()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if config.Options.PolicyVersion != 2 {
+ t.Error("Expected PolicyVersion 2")
+ }
+}
diff --git a/actions/context.go b/actions/context.go
index 8ad1357..4253de2 100644
--- a/actions/context.go
+++ b/actions/context.go
@@ -22,33 +22,26 @@
// All of the actions include a significant amount of logging, so that good
// output can be provided for cmd/fscrypt's verbose mode.
// The top-level actions currently include:
-// - Creating a new config file
-// - Creating a context on which to perform actions
-// - Creating, unlocking, and modifying Protectors
-// - Creating, unlocking, and modifying Policies
+// - Creating a new config file
+// - Creating a context on which to perform actions
+// - Creating, unlocking, and modifying Protectors
+// - Creating, unlocking, and modifying Policies
package actions
import (
"log"
"os/user"
- "golang.org/x/sys/unix"
-
"github.com/pkg/errors"
"github.com/google/fscrypt/filesystem"
+ "github.com/google/fscrypt/keyring"
"github.com/google/fscrypt/metadata"
"github.com/google/fscrypt/util"
)
-// Errors relating to Config files or Config structures.
-var (
- ErrNoConfigFile = errors.New("global config file does not exist")
- ErrBadConfigFile = errors.New("global config file has invalid data")
- ErrConfigFileExists = errors.New("global config file already exists")
- ErrBadConfig = errors.New("invalid Config structure provided")
- ErrLocked = errors.New("key needs to be unlocked first")
-)
+// ErrLocked indicates that the key hasn't been unwrapped yet.
+var ErrLocked = errors.New("key needs to be unlocked first")
// Context contains the necessary global state to perform most of fscrypt's
// actions.
@@ -56,20 +49,29 @@ type Context struct {
// Config is the struct loaded from the global config file. It can be
// modified after being loaded to customise parameters.
Config *metadata.Config
- // Mount is the filesystem relitive to which all Protectors and Policies
- // are added, edited, removed, and applied.
+ // Mount is the filesystem relative to which all Protectors and Policies
+ // are added, edited, removed, and applied, and to which policies using
+ // the filesystem keyring are provisioned.
Mount *filesystem.Mount
- // TargetUser is the user for which protectors are created and to whose
- // keyring policies are provisioned.
+ // TargetUser is the user for whom protectors are created, and to whose
+ // keyring policies using the user keyring are provisioned. It's also
+ // the user for whom the keys are claimed in the filesystem keyring when
+ // v2 policies are provisioned.
TargetUser *user.User
+ // TrustedUser is the user for whom policies and protectors are allowed
+ // to be read. Specifically, if TrustedUser is set, then only
+ // policies and protectors owned by TrustedUser or by root will be
+ // allowed to be read. If it's nil, then all policies and protectors
+ // the process has filesystem-level read access to will be allowed.
+ TrustedUser *user.User
}
// NewContextFromPath makes a context for the filesystem containing the
// specified path and whose Config is loaded from the global config file. On
-// success, the Context contains a valid Config and Mount. The target defaults
-// the the current effective user if none is specified.
-func NewContextFromPath(path string, target *user.User) (*Context, error) {
- ctx, err := newContextFromUser(target)
+// success, the Context contains a valid Config and Mount. The target user
+// defaults to the current effective user if none is specified.
+func NewContextFromPath(path string, targetUser *user.User) (*Context, error) {
+ ctx, err := newContextFromUser(targetUser)
if err != nil {
return nil, err
}
@@ -78,16 +80,16 @@ func NewContextFromPath(path string, target *user.User) (*Context, error) {
}
log.Printf("%s is on %s filesystem %q (%s)", path,
- ctx.Mount.Filesystem, ctx.Mount.Path, ctx.Mount.Device)
+ ctx.Mount.FilesystemType, ctx.Mount.Path, ctx.Mount.Device)
return ctx, nil
}
// NewContextFromMountpoint makes a context for the filesystem at the specified
// mountpoint and whose Config is loaded from the global config file. On
-// success, the Context contains a valid Config and Mount. The target defaults
-// the the current effective user if none is specified.
-func NewContextFromMountpoint(mountpoint string, target *user.User) (*Context, error) {
- ctx, err := newContextFromUser(target)
+// success, the Context contains a valid Config and Mount. The target user
+// defaults to the current effective user if none is specified.
+func NewContextFromMountpoint(mountpoint string, targetUser *user.User) (*Context, error) {
+ ctx, err := newContextFromUser(targetUser)
if err != nil {
return nil, err
}
@@ -95,60 +97,62 @@ func NewContextFromMountpoint(mountpoint string, target *user.User) (*Context, e
return nil, err
}
- log.Printf("found %s filesystem %q (%s)", ctx.Mount.Filesystem,
+ log.Printf("found %s filesystem %q (%s)", ctx.Mount.FilesystemType,
ctx.Mount.Path, ctx.Mount.Device)
return ctx, nil
}
// newContextFromUser makes a context with the corresponding target user, and
-// whose Config is loaded from the global config file. If the target is nil, the
-// effecitive user is used.
-func newContextFromUser(target *user.User) (*Context, error) {
+// whose Config is loaded from the global config file. If the target user is
+// nil, the effective user is used.
+func newContextFromUser(targetUser *user.User) (*Context, error) {
var err error
- if target == nil {
- if target, err = util.EffectiveUser(); err != nil {
+ if targetUser == nil {
+ if targetUser, err = util.EffectiveUser(); err != nil {
return nil, err
}
}
- ctx := &Context{TargetUser: target}
+ ctx := &Context{TargetUser: targetUser}
if ctx.Config, err = getConfig(); err != nil {
return nil, err
}
- log.Printf("creating context for %q", target.Username)
+ // By default, when running as a non-root user we only read policies and
+ // protectors owned by the user or root. When running as root, we allow
+ // reading all policies and protectors.
+ if !ctx.Config.GetAllowCrossUserMetadata() && !util.IsUserRoot() {
+ ctx.TrustedUser, err = util.EffectiveUser()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ log.Printf("creating context for user %q", targetUser.Username)
return ctx, nil
}
-// checkContext verifies that the context contains an valid config and a mount
+// checkContext verifies that the context contains a valid config and a mount
// which is being used with fscrypt.
func (ctx *Context) checkContext() error {
if err := ctx.Config.CheckValidity(); err != nil {
- return errors.Wrap(ErrBadConfig, err.Error())
+ return &ErrBadConfig{ctx.Config, err}
}
- return ctx.Mount.CheckSetup()
+ return ctx.Mount.CheckSetup(ctx.TrustedUser)
}
-// getService returns the keyring service for this context. We use the presence
-// of the LegacyConfig flag to determine if we should use the legacy services.
-// For ext4 systems before v4.8 and f2fs systems before v4.6, filesystem
-// specific services must be used (these legacy services will still work with
-// later kernels).
-func (ctx *Context) getService() string {
- // For legacy configurations, we may need non-standard services
- if ctx.Config.HasCompatibilityOption(LegacyConfig) {
- switch ctx.Mount.Filesystem {
- case "ext4", "f2fs":
- return ctx.Mount.Filesystem + ":"
- }
+func (ctx *Context) getKeyringOptions() *keyring.Options {
+ return &keyring.Options{
+ Mount: ctx.Mount,
+ User: ctx.TargetUser,
+ UseFsKeyringForV1Policies: ctx.Config.GetUseFsKeyringForV1Policies(),
}
- return unix.FS_KEY_DESC_PREFIX
}
// getProtectorOption returns the ProtectorOption for the protector on the
// context's mountpoint with the specified descriptor.
func (ctx *Context) getProtectorOption(protectorDescriptor string) *ProtectorOption {
- mnt, data, err := ctx.Mount.GetProtector(protectorDescriptor)
+ mnt, data, err := ctx.Mount.GetProtector(protectorDescriptor, ctx.TrustedUser)
if err != nil {
return &ProtectorOption{ProtectorInfo{}, nil, err}
}
@@ -167,7 +171,7 @@ func (ctx *Context) ProtectorOptions() ([]*ProtectorOption, error) {
if err := ctx.checkContext(); err != nil {
return nil, err
}
- descriptors, err := ctx.Mount.ListProtectors()
+ descriptors, err := ctx.Mount.ListProtectors(ctx.TrustedUser)
if err != nil {
return nil, err
}
diff --git a/actions/context_test.go b/actions/context_test.go
index 593518f..6e28857 100644
--- a/actions/context_test.go
+++ b/actions/context_test.go
@@ -1,5 +1,5 @@
/*
- * config_test.go - tests for creating new contexts
+ * context_test.go - tests for creating new contexts
*
* Copyright 2017 Google Inc.
* Author: Joe Richey (joerichey@google.com)
@@ -27,6 +27,7 @@ import (
"testing"
"time"
+ "github.com/google/fscrypt/filesystem"
"github.com/google/fscrypt/util"
"github.com/pkg/errors"
)
@@ -47,12 +48,13 @@ func setupContext() (ctx *Context, err error) {
ConfigFileLocation = filepath.Join(mountpoint, "test.conf")
// Should not be able to setup without a config file
+ os.Remove(ConfigFileLocation)
if badCtx, badCtxErr := NewContextFromMountpoint(mountpoint, nil); badCtxErr == nil {
badCtx.Mount.RemoveAllMetadata()
return nil, fmt.Errorf("created context at %q without config file", badCtx.Mount.Path)
}
- if err = CreateConfigFile(testTime, true); err != nil {
+ if err = CreateConfigFile(testTime, 0); err != nil {
return nil, err
}
defer func() {
@@ -66,7 +68,7 @@ func setupContext() (ctx *Context, err error) {
return nil, err
}
- return ctx, ctx.Mount.Setup()
+ return ctx, ctx.Mount.Setup(filesystem.WorldWritable)
}
// Cleans up the testing config file and testing filesystem data.
diff --git a/actions/hashing_test.go b/actions/hashing_test.go
index d249d8b..26f627b 100644
--- a/actions/hashing_test.go
+++ b/actions/hashing_test.go
@@ -20,7 +20,7 @@
package actions
import (
- "io/ioutil"
+ "io"
"log"
"testing"
"time"
@@ -43,10 +43,10 @@ func TestCostsSearch(t *testing.T) {
t.Error(err)
}
- if actual*2 < target {
+ if actual*3 < target {
t.Errorf("actual=%v is too small (target=%v)", actual, target)
}
- if target*2 < actual {
+ if target*3 < actual {
t.Errorf("actual=%v is too big (target=%v)", actual, target)
}
}
@@ -54,7 +54,7 @@ func TestCostsSearch(t *testing.T) {
func benchmarkCostsSearch(b *testing.B, target time.Duration) {
// Disable logging for benchmarks
- log.SetOutput(ioutil.Discard)
+ log.SetOutput(io.Discard)
for i := 0; i < b.N; i++ {
_, err := getHashingCosts(target)
if err != nil {
diff --git a/actions/policy.go b/actions/policy.go
index cbdcb3a..d745f8b 100644
--- a/actions/policy.go
+++ b/actions/policy.go
@@ -1,5 +1,5 @@
/*
- * protector.go - functions for dealing with policies
+ * policy.go - functions for dealing with policies
*
* Copyright 2017 Google Inc.
* Author: Joe Richey (joerichey@google.com)
@@ -22,46 +22,147 @@ package actions
import (
"fmt"
"log"
+ "os"
+ "os/user"
"reflect"
"github.com/pkg/errors"
+ "google.golang.org/protobuf/proto"
"github.com/google/fscrypt/crypto"
"github.com/google/fscrypt/filesystem"
+ "github.com/google/fscrypt/keyring"
"github.com/google/fscrypt/metadata"
- "github.com/google/fscrypt/security"
"github.com/google/fscrypt/util"
)
-// Errors relating to Policies
-var (
- ErrMissingPolicyMetadata = util.SystemError("missing policy metadata for encrypted directory")
- ErrPolicyMetadataMismatch = util.SystemError("inconsistent metadata between filesystem and directory")
- ErrDifferentFilesystem = errors.New("policies may only protect files on the same filesystem")
- ErrOnlyProtector = errors.New("cannot remove the only protector for a policy")
- ErrAlreadyProtected = errors.New("policy already protected by protector")
- ErrNotProtected = errors.New("policy not protected by protector")
-)
+// ErrAccessDeniedPossiblyV2 indicates that a directory's encryption policy
+// couldn't be retrieved due to "permission denied", but it looks like it's due
+// to the directory using a v2 policy but the kernel not supporting it.
+type ErrAccessDeniedPossiblyV2 struct {
+ DirPath string
+}
+
+func (err *ErrAccessDeniedPossiblyV2) Error() string {
+ return fmt.Sprintf(`
+ failed to get encryption policy of %s: permission denied
+
+ This may be caused by the directory using a v2 encryption policy and the
+ current kernel not supporting it. If indeed the case, then this
+ directory can only be used on kernel v5.4 and later. You can create
+ directories accessible on older kernels by changing policy_version to 1
+ in %s.`,
+ err.DirPath, ConfigFileLocation)
+}
+
+// ErrAlreadyProtected indicates that a policy is already protected by the given
+// protector.
+type ErrAlreadyProtected struct {
+ Policy *Policy
+ Protector *Protector
+}
+
+func (err *ErrAlreadyProtected) Error() string {
+ return fmt.Sprintf("policy %s is already protected by protector %s",
+ err.Policy.Descriptor(), err.Protector.Descriptor())
+}
+
+// ErrDifferentFilesystem indicates that a policy can't be applied to a
+// directory on a different filesystem.
+type ErrDifferentFilesystem struct {
+ PolicyMount *filesystem.Mount
+ PathMount *filesystem.Mount
+}
+
+func (err *ErrDifferentFilesystem) Error() string {
+ return fmt.Sprintf(`cannot apply policy from filesystem %q to a
+ directory on filesystem %q. Policies may only protect files on the same
+ filesystem.`, err.PolicyMount.Path, err.PathMount.Path)
+}
+
+// ErrMissingPolicyMetadata indicates that a directory is encrypted but its
+// policy metadata cannot be found.
+type ErrMissingPolicyMetadata struct {
+ Mount *filesystem.Mount
+ DirPath string
+ Descriptor string
+}
+
+func (err *ErrMissingPolicyMetadata) Error() string {
+ return fmt.Sprintf(`filesystem %q does not contain the policy metadata
+ for %q. This directory has either been encrypted with another tool (such
+ as e4crypt), or the file %q has been deleted.`,
+ err.Mount.Path, err.DirPath,
+ err.Mount.PolicyPath(err.Descriptor))
+}
+
+// ErrNotProtected indicates that the given policy is not protected by the given
+// protector.
+type ErrNotProtected struct {
+ PolicyDescriptor string
+ ProtectorDescriptor string
+}
+
+func (err *ErrNotProtected) Error() string {
+ return fmt.Sprintf(`policy %s is not protected by protector %s`,
+ err.PolicyDescriptor, err.ProtectorDescriptor)
+}
+
+// ErrOnlyProtector indicates that the last protector can't be removed from a
+// policy.
+type ErrOnlyProtector struct {
+ Policy *Policy
+}
+
+func (err *ErrOnlyProtector) Error() string {
+ return fmt.Sprintf(`cannot remove the only protector from policy %s. A
+ policy must have at least one protector.`, err.Policy.Descriptor())
+}
+
+// ErrPolicyMetadataMismatch indicates that the policy metadata for an encrypted
+// directory is inconsistent with that directory.
+type ErrPolicyMetadataMismatch struct {
+ DirPath string
+ Mount *filesystem.Mount
+ PathData *metadata.PolicyData
+ MountData *metadata.PolicyData
+}
+
+func (err *ErrPolicyMetadataMismatch) Error() string {
+ return fmt.Sprintf(`inconsistent metadata between encrypted directory %q
+ and its corresponding metadata file %q.
+
+ Directory has descriptor:%s %s
+
+ Metadata file has descriptor:%s %s`,
+ err.DirPath, err.Mount.PolicyPath(err.PathData.KeyDescriptor),
+ err.PathData.KeyDescriptor, err.PathData.Options,
+ err.MountData.KeyDescriptor, err.MountData.Options)
+}
// PurgeAllPolicies removes all policy keys on the filesystem from the kernel
-// keyring. In order for this removal to have an effect, the filesystem should
-// also be unmounted.
+// keyring. In order for this to fully take effect, the filesystem may also need
+// to be unmounted or caches dropped.
func PurgeAllPolicies(ctx *Context) error {
if err := ctx.checkContext(); err != nil {
return err
}
- policies, err := ctx.Mount.ListPolicies()
+ policies, err := ctx.Mount.ListPolicies(nil)
if err != nil {
return err
}
for _, policyDescriptor := range policies {
- service := ctx.getService()
- err = security.RemoveKey(service+policyDescriptor, ctx.TargetUser)
-
+ err = keyring.RemoveEncryptionKey(policyDescriptor, ctx.getKeyringOptions(), false)
switch errors.Cause(err) {
- case nil, security.ErrKeySearch:
+ case nil, keyring.ErrKeyNotPresent:
// We don't care if the key has already been removed
+ case keyring.ErrKeyFilesOpen:
+ log.Printf("Key for policy %s couldn't be fully removed because some files are still in-use",
+ policyDescriptor)
+ case keyring.ErrKeyAddedByOtherUsers:
+ log.Printf("Key for policy %s couldn't be fully removed because other user(s) have added it too",
+ policyDescriptor)
default:
return err
}
@@ -75,10 +176,12 @@ func PurgeAllPolicies(ctx *Context) error {
// allow encrypted files to be accessed). As with the key struct, a Policy
// should be wiped after use.
type Policy struct {
- Context *Context
- data *metadata.PolicyData
- key *crypto.Key
- created bool
+ Context *Context
+ data *metadata.PolicyData
+ key *crypto.Key
+ created bool
+ ownerIfCreating *user.User
+ newLinkedProtectors []string
}
// CreatePolicy creates a Policy protected by given Protector and stores the
@@ -94,16 +197,28 @@ func CreatePolicy(ctx *Context, protector *Protector) (*Policy, error) {
return nil, err
}
+ keyDescriptor, err := crypto.ComputeKeyDescriptor(key, ctx.Config.Options.PolicyVersion)
+ if err != nil {
+ key.Wipe()
+ return nil, err
+ }
+
policy := &Policy{
Context: ctx,
data: &metadata.PolicyData{
Options: ctx.Config.Options,
- KeyDescriptor: crypto.ComputeDescriptor(key),
+ KeyDescriptor: keyDescriptor,
},
key: key,
created: true,
}
+ policy.ownerIfCreating, err = getOwnerOfMetadataForProtector(protector)
+ if err != nil {
+ policy.Lock()
+ return nil, err
+ }
+
if err = policy.AddProtector(protector); err != nil {
policy.Lock()
return nil, err
@@ -119,7 +234,7 @@ func GetPolicy(ctx *Context, descriptor string) (*Policy, error) {
if err := ctx.checkContext(); err != nil {
return nil, err
}
- data, err := ctx.Mount.GetPolicy(descriptor)
+ data, err := ctx.Mount.GetPolicy(descriptor, ctx.TrustedUser)
if err != nil {
return nil, err
}
@@ -140,23 +255,35 @@ func GetPolicyFromPath(ctx *Context, path string) (*Policy, error) {
// We double check that the options agree for both the data we get from
// the path, and the data we get from the mountpoint.
pathData, err := metadata.GetPolicy(path)
+ err = ctx.Mount.EncryptionSupportError(err)
if err != nil {
+ // On kernels that don't support v2 encryption policies, trying
+ // to open a directory with a v2 policy simply gave EACCES. This
+ // is ambiguous with other errors, but try to detect this case
+ // and show a better error message.
+ if os.IsPermission(err) &&
+ filesystem.HaveReadAccessTo(path) &&
+ !keyring.IsFsKeyringSupported(ctx.Mount) {
+ return nil, &ErrAccessDeniedPossiblyV2{path}
+ }
return nil, err
}
descriptor := pathData.KeyDescriptor
log.Printf("found policy %s for %q", descriptor, path)
- mountData, err := ctx.Mount.GetPolicy(descriptor)
+ mountData, err := ctx.Mount.GetPolicy(descriptor, ctx.TrustedUser)
if err != nil {
log.Printf("getting policy metadata: %v", err)
- return nil, errors.Wrap(ErrMissingPolicyMetadata, path)
+ if _, ok := err.(*filesystem.ErrPolicyNotFound); ok {
+ return nil, &ErrMissingPolicyMetadata{ctx.Mount, path, descriptor}
+ }
+ return nil, err
}
log.Printf("found data for policy %s on %q", descriptor, ctx.Mount.Path)
- if !reflect.DeepEqual(pathData.Options, mountData.Options) {
- log.Printf("options from path: %+v", pathData.Options)
- log.Printf("options from mount: %+v", mountData.Options)
- return nil, errors.Wrapf(ErrPolicyMetadataMismatch, "policy %s", descriptor)
+ if !proto.Equal(pathData.Options, mountData.Options) ||
+ pathData.KeyDescriptor != mountData.KeyDescriptor {
+ return nil, &ErrPolicyMetadataMismatch{path, ctx.Mount, pathData, mountData}
}
log.Print("data from filesystem and path agree")
@@ -188,15 +315,23 @@ func (policy *Policy) Descriptor() string {
return policy.data.KeyDescriptor
}
-// Description returns the description that will be used when the key for this
-// Policy is inserted into the keyring
-func (policy *Policy) Description() string {
- return policy.Context.getService() + policy.Descriptor()
+// Options returns the encryption options of this policy.
+func (policy *Policy) Options() *metadata.EncryptionOptions {
+ return policy.data.Options
+}
+
+// Version returns the version of this policy.
+func (policy *Policy) Version() int64 {
+ return policy.data.Options.PolicyVersion
}
-// Destroy removes a policy from the filesystem. The internal key should still
-// be wiped with Lock().
+// Destroy removes a policy from the filesystem. It also removes any new
+// protector links that were created for the policy. This does *not* wipe the
+// policy's internal key from memory; use Lock() to do that.
func (policy *Policy) Destroy() error {
+ for _, protectorDescriptor := range policy.newLinkedProtectors {
+ policy.Context.Mount.RemoveProtector(protectorDescriptor)
+ }
return policy.Context.Mount.RemovePolicy(policy.Descriptor())
}
@@ -260,7 +395,7 @@ func (policy *Policy) UnlockWithProtector(protector *Protector) error {
}
idx, ok := policy.findWrappedKeyIndex(protector.Descriptor())
if !ok {
- return ErrNotProtected
+ return &ErrNotProtected{policy.Descriptor(), protector.Descriptor()}
}
var err error
@@ -284,6 +419,25 @@ func (policy *Policy) UsesProtector(protector *Protector) bool {
return ok
}
+// getOwnerOfMetadataForProtector returns the User to whom the owner of any new
+// policies or protector links for the given protector should be set.
+//
+// This will return a non-nil value only when the protector is a login protector
+// and the process is running as root. In this scenario, root is setting up
+// encryption on the user's behalf, so we need to make new policies and
+// protector links owned by the user (rather than root) to allow them to be read
+// by the user, just like the login protector itself which is handled elsewhere.
+func getOwnerOfMetadataForProtector(protector *Protector) (*user.User, error) {
+ if protector.data.Source == metadata.SourceType_pam_passphrase && util.IsUserRoot() {
+ owner, err := util.UserFromUID(protector.data.Uid)
+ if err != nil {
+ return nil, err
+ }
+ return owner, nil
+ }
+ return nil, nil
+}
+
// 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
@@ -291,7 +445,7 @@ func (policy *Policy) UsesProtector(protector *Protector) bool {
// protector must both be unlocked.
func (policy *Policy) AddProtector(protector *Protector) error {
if policy.UsesProtector(protector) {
- return ErrAlreadyProtected
+ return &ErrAlreadyProtected{policy, protector}
}
if policy.key == nil || protector.key == nil {
return ErrLocked
@@ -299,13 +453,22 @@ func (policy *Policy) AddProtector(protector *Protector) error {
// If the protector is on a different filesystem, we need to add a link
// to it on the policy's filesystem.
- if policy.Context.Mount != protector.Context.Mount {
+ if !reflect.DeepEqual(policy.Context.Mount, protector.Context.Mount) {
log.Printf("policy on %s\n protector on %s\n", policy.Context.Mount, protector.Context.Mount)
- err := policy.Context.Mount.AddLinkedProtector(
- protector.Descriptor(), protector.Context.Mount)
+ ownerIfCreating, err := getOwnerOfMetadataForProtector(protector)
+ if err != nil {
+ return err
+ }
+ isNewLink, err := policy.Context.Mount.AddLinkedProtector(
+ protector.Descriptor(), protector.Context.Mount,
+ protector.Context.TrustedUser, ownerIfCreating)
if err != nil {
return err
}
+ if isNewLink {
+ policy.newLinkedProtectors = append(policy.newLinkedProtectors,
+ protector.Descriptor())
+ }
} else {
log.Printf("policy and protector both on %q", policy.Context.Mount)
}
@@ -331,18 +494,19 @@ func (policy *Policy) AddProtector(protector *Protector) error {
}
// RemoveProtector updates the data that is wrapping the Policy Key so that the
-// provided Protector is no longer protecting the specified Policy. If an error
-// is returned, no data has been changed. Note that no protector links are
+// protector with the given descriptor is no longer protecting the specified
+// Policy. If an error is returned, no data has been changed. Note that the
+// protector itself won't be removed, nor will a link to the protector be
// removed (in the case where the protector and policy are on different
-// filesystems). The policy and protector can be locked or unlocked.
-func (policy *Policy) RemoveProtector(protector *Protector) error {
- idx, ok := policy.findWrappedKeyIndex(protector.Descriptor())
+// filesystems). The policy can be locked or unlocked.
+func (policy *Policy) RemoveProtector(protectorDescriptor string) error {
+ idx, ok := policy.findWrappedKeyIndex(protectorDescriptor)
if !ok {
- return ErrNotProtected
+ return &ErrNotProtected{policy.Descriptor(), protectorDescriptor}
}
if len(policy.data.WrappedPolicyKeys) == 1 {
- return ErrOnlyProtector
+ return &ErrOnlyProtector{policy}
}
// Remove the wrapped key from the data
@@ -362,18 +526,26 @@ func (policy *Policy) RemoveProtector(protector *Protector) error {
func (policy *Policy) Apply(path string) error {
if pathMount, err := filesystem.FindMount(path); err != nil {
return err
- } else if pathMount != policy.Context.Mount {
- return ErrDifferentFilesystem
+ } else if !reflect.DeepEqual(pathMount, policy.Context.Mount) {
+ return &ErrDifferentFilesystem{policy.Context.Mount, pathMount}
}
- return metadata.SetPolicy(path, policy.data)
+ err := metadata.SetPolicy(path, policy.data)
+ return policy.Context.Mount.EncryptionSupportError(err)
}
-// IsProvisioned returns a boolean indicating if the policy has its key in the
-// keyring, meaning files and directories using this policy are accessible.
-func (policy *Policy) IsProvisioned() bool {
- _, err := security.FindKey(policy.Description(), policy.Context.TargetUser)
- return err == nil
+// GetProvisioningStatus returns the status of this policy's key in the keyring.
+func (policy *Policy) GetProvisioningStatus() keyring.KeyStatus {
+ status, _ := keyring.GetEncryptionKeyStatus(policy.Descriptor(),
+ policy.Context.getKeyringOptions())
+ return status
+}
+
+// IsProvisionedByTargetUser returns true if the policy's key is present in the
+// target kernel keyring, but not if that keyring is a filesystem keyring and
+// the key only been added by users other than Context.TargetUser.
+func (policy *Policy) IsProvisionedByTargetUser() bool {
+ return policy.GetProvisioningStatus() == keyring.KeyPresent
}
// Provision inserts the Policy key into the kernel keyring. This allows reading
@@ -382,18 +554,40 @@ func (policy *Policy) Provision() error {
if policy.key == nil {
return ErrLocked
}
- return crypto.InsertPolicyKey(policy.key, policy.Description(), policy.Context.TargetUser)
+ return keyring.AddEncryptionKey(policy.key, policy.Descriptor(),
+ policy.Context.getKeyringOptions())
}
// Deprovision removes the Policy key from the kernel keyring. This prevents
-// reading and writing to the directory once the caches are cleared.
-func (policy *Policy) Deprovision() error {
- return security.RemoveKey(policy.Description(), policy.Context.TargetUser)
+// reading and writing to the directory --- unless the target keyring is a user
+// keyring, in which case caches must be dropped too. If the Policy key was
+// already removed, returns keyring.ErrKeyNotPresent.
+func (policy *Policy) Deprovision(allUsers bool) error {
+ return keyring.RemoveEncryptionKey(policy.Descriptor(),
+ policy.Context.getKeyringOptions(), allUsers)
+}
+
+// NeedsUserKeyring returns true if Provision and Deprovision for this policy
+// will use a user keyring (deprecated), not a filesystem keyring.
+func (policy *Policy) NeedsUserKeyring() bool {
+ return policy.Version() == 1 && !policy.Context.Config.GetUseFsKeyringForV1Policies()
+}
+
+// NeedsRootToProvision returns true if Provision and Deprovision will require
+// root for this policy in the current configuration.
+func (policy *Policy) NeedsRootToProvision() bool {
+ return policy.Version() == 1 && policy.Context.Config.GetUseFsKeyringForV1Policies()
+}
+
+// CanBeAppliedWithoutProvisioning returns true if this process can apply this
+// policy to a directory without first calling Provision.
+func (policy *Policy) CanBeAppliedWithoutProvisioning() bool {
+ return policy.Version() == 1 || util.IsUserRoot()
}
// commitData writes the Policy's current data to the filesystem.
func (policy *Policy) commitData() error {
- return policy.Context.Mount.AddPolicy(policy.data)
+ return policy.Context.Mount.AddPolicy(policy.data, policy.ownerIfCreating)
}
// findWrappedPolicyKey returns the index of the wrapped policy key
@@ -413,7 +607,7 @@ func (policy *Policy) addKey(toAdd *metadata.WrappedPolicyKey) {
policy.data.WrappedPolicyKeys = append(policy.data.WrappedPolicyKeys, toAdd)
}
-// remove removes the wrapped policy key at the specified index. This
+// removeKey removes the wrapped policy key at the specified index. This
// does not preserve the order of the wrapped policy key array. If no index is
// specified the last key is removed.
func (policy *Policy) removeKey(index int) *metadata.WrappedPolicyKey {
diff --git a/actions/policy_test.go b/actions/policy_test.go
index 11c9c3e..8248862 100644
--- a/actions/policy_test.go
+++ b/actions/policy_test.go
@@ -27,7 +27,7 @@ import (
// Makes a protector and policy
func makeBoth() (*Protector, *Policy, error) {
- protector, err := CreateProtector(testContext, testProtectorName, goodCallback)
+ protector, err := CreateProtector(testContext, testProtectorName, goodCallback, nil)
if err != nil {
return nil, nil, err
}
@@ -68,7 +68,7 @@ func TestPolicyGoodAddProtector(t *testing.T) {
t.Fatal(err)
}
- pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback)
+ pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback, nil)
if err != nil {
t.Fatal(err)
}
@@ -103,7 +103,7 @@ func TestPolicyGoodRemoveProtector(t *testing.T) {
t.Fatal(err)
}
- pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback)
+ pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback, nil)
if err != nil {
t.Fatal(err)
}
@@ -114,7 +114,7 @@ func TestPolicyGoodRemoveProtector(t *testing.T) {
t.Fatal(err)
}
- err = pol.RemoveProtector(pro1)
+ err = pol.RemoveProtector(pro1.Descriptor())
if err != nil {
t.Error(err)
}
@@ -129,17 +129,17 @@ func TestPolicyBadRemoveProtector(t *testing.T) {
t.Fatal(err)
}
- pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback)
+ pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback, nil)
if err != nil {
t.Fatal(err)
}
defer cleanupProtector(pro2)
- if pol.RemoveProtector(pro2) == nil {
+ if pol.RemoveProtector(pro2.Descriptor()) == nil {
t.Error("we should not be able to remove a protector we did not add")
}
- if pol.RemoveProtector(pro1) == nil {
+ if pol.RemoveProtector(pro1.Descriptor()) == nil {
t.Error("we should not be able to remove all the protectors from a policy")
}
}
diff --git a/actions/protector.go b/actions/protector.go
index ffc3c43..b986eb0 100644
--- a/actions/protector.go
+++ b/actions/protector.go
@@ -22,21 +22,55 @@ package actions
import (
"fmt"
"log"
-
- "github.com/pkg/errors"
+ "os/user"
"github.com/google/fscrypt/crypto"
"github.com/google/fscrypt/metadata"
"github.com/google/fscrypt/util"
)
-// Errors relating to Protectors
-var (
- ErrProtectorName = errors.New("login protectors do not need a name")
- ErrMissingProtectorName = errors.New("custom protectors must have a name")
- ErrDuplicateName = errors.New("protector with this name already exists")
- ErrDuplicateUID = errors.New("login protector for this user already exists")
-)
+// LoginProtectorMountpoint is the mountpoint where login protectors are stored.
+// This can be overridden by the user of this package.
+var LoginProtectorMountpoint = "/"
+
+// ErrLoginProtectorExists indicates that a user already has a login protector.
+type ErrLoginProtectorExists struct {
+ User *user.User
+}
+
+func (err *ErrLoginProtectorExists) Error() string {
+ return fmt.Sprintf("user %q already has a login protector", err.User.Username)
+}
+
+// ErrLoginProtectorName indicates that a name was given for a login protector.
+type ErrLoginProtectorName struct {
+ Name string
+ User *user.User
+}
+
+func (err *ErrLoginProtectorName) Error() string {
+ return fmt.Sprintf(`cannot assign name %q to new login protector for
+ user %q because login protectors are identified by user, not by name.`,
+ err.Name, err.User.Username)
+}
+
+// ErrMissingProtectorName indicates that a protector name is needed.
+type ErrMissingProtectorName struct {
+ Source metadata.SourceType
+}
+
+func (err *ErrMissingProtectorName) Error() string {
+ return fmt.Sprintf("%s protectors must be named", err.Source)
+}
+
+// ErrProtectorNameExists indicates that a protector name already exists.
+type ErrProtectorNameExists struct {
+ Name string
+}
+
+func (err *ErrProtectorNameExists) Error() string {
+ return fmt.Sprintf("there is already a protector named %q", err.Name)
+}
// checkForProtectorWithName returns an error if there is already a protector
// on the filesystem with a specific name (or if we cannot read the necessary
@@ -48,7 +82,7 @@ func checkForProtectorWithName(ctx *Context, name string) error {
}
for _, option := range options {
if option.Name() == name {
- return errors.Wrapf(ErrDuplicateName, "name %q", name)
+ return &ErrProtectorNameExists{name}
}
}
return nil
@@ -64,7 +98,7 @@ func checkIfUserHasLoginProtector(ctx *Context, uid int64) error {
}
for _, option := range options {
if option.Source() == metadata.SourceType_pam_passphrase && option.UID() == uid {
- return errors.Wrapf(ErrDuplicateUID, "user %q", ctx.TargetUser.Username)
+ return &ErrLoginProtectorExists{ctx.TargetUser}
}
}
return nil
@@ -75,17 +109,18 @@ func checkIfUserHasLoginProtector(ctx *Context, uid int64) error {
// to unlock policies and create new polices. As with the key struct, a
// Protector should be wiped after use.
type Protector struct {
- Context *Context
- data *metadata.ProtectorData
- key *crypto.Key
- created bool
+ Context *Context
+ data *metadata.ProtectorData
+ key *crypto.Key
+ created bool
+ ownerIfCreating *user.User
}
// CreateProtector creates an unlocked protector with a given name (name only
// needed for custom and raw protector types). The keyFn provided to create the
// Protector key will only be called once. If an error is returned, no data has
// been changed on the filesystem.
-func CreateProtector(ctx *Context, name string, keyFn KeyFunc) (*Protector, error) {
+func CreateProtector(ctx *Context, name string, keyFn KeyFunc, owner *user.User) (*Protector, error) {
if err := ctx.checkContext(); err != nil {
return nil, err
}
@@ -93,12 +128,12 @@ func CreateProtector(ctx *Context, name string, keyFn KeyFunc) (*Protector, erro
if ctx.Config.Source == metadata.SourceType_pam_passphrase {
// login protectors don't need a name (we use the username instead)
if name != "" {
- return nil, ErrProtectorName
+ return nil, &ErrLoginProtectorName{name, ctx.TargetUser}
}
} else {
// non-login protectors need a name (so we can distinguish between them)
if name == "" {
- return nil, ErrMissingProtectorName
+ return nil, &ErrMissingProtectorName{ctx.Config.Source}
}
// we don't want to duplicate naming
if err := checkForProtectorWithName(ctx, name); err != nil {
@@ -113,7 +148,8 @@ func CreateProtector(ctx *Context, name string, keyFn KeyFunc) (*Protector, erro
Name: name,
Source: ctx.Config.Source,
},
- created: true,
+ created: true,
+ ownerIfCreating: owner,
}
// Extra data is needed for some SourceTypes
@@ -123,7 +159,7 @@ func CreateProtector(ctx *Context, name string, keyFn KeyFunc) (*Protector, erro
// UID for this kind of source.
protector.data.Uid = int64(util.AtoiOrPanic(ctx.TargetUser.Uid))
// Make sure we aren't duplicating protectors
- if err := checkIfUserHasLoginProtector(ctx, protector.data.Uid); err != nil {
+ if err = checkIfUserHasLoginProtector(ctx, protector.data.Uid); err != nil {
return nil, err
}
fallthrough
@@ -140,9 +176,13 @@ func CreateProtector(ctx *Context, name string, keyFn KeyFunc) (*Protector, erro
if protector.key, err = crypto.NewRandomKey(metadata.InternalKeyLen); err != nil {
return nil, err
}
- protector.data.ProtectorDescriptor = crypto.ComputeDescriptor(protector.key)
+ protector.data.ProtectorDescriptor, err = crypto.ComputeKeyDescriptor(protector.key, 1)
+ if err != nil {
+ protector.Lock()
+ return nil, err
+ }
- if err := protector.Rewrap(keyFn); err != nil {
+ if err = protector.Rewrap(keyFn); err != nil {
protector.Lock()
return nil, err
}
@@ -161,7 +201,7 @@ func GetProtector(ctx *Context, descriptor string) (*Protector, error) {
}
protector := &Protector{Context: ctx}
- protector.data, err = ctx.Mount.GetRegularProtector(descriptor)
+ protector.data, err = ctx.Mount.GetRegularProtector(descriptor, ctx.TrustedUser)
return protector, err
}
@@ -180,7 +220,7 @@ func GetProtectorFromOption(ctx *Context, option *ProtectorOption) (*Protector,
// Replace the context if this is a linked protector
if option.LinkedMount != nil {
- ctx = &Context{ctx.Config, option.LinkedMount, ctx.TargetUser}
+ ctx = &Context{ctx.Config, option.LinkedMount, ctx.TargetUser, ctx.TrustedUser}
}
return &Protector{Context: ctx, data: option.data}, nil
}
@@ -256,5 +296,5 @@ func (protector *Protector) Rewrap(keyFn KeyFunc) error {
return err
}
- return protector.Context.Mount.AddProtector(protector.data)
+ return protector.Context.Mount.AddProtector(protector.data, protector.ownerIfCreating)
}
diff --git a/actions/protector_test.go b/actions/protector_test.go
index 6c81d49..f20dbcf 100644
--- a/actions/protector_test.go
+++ b/actions/protector_test.go
@@ -43,7 +43,7 @@ func badCallback(info ProtectorInfo, retry bool) (*crypto.Key, error) {
// Tests that we can create a valid protector.
func TestCreateProtector(t *testing.T) {
- p, err := CreateProtector(testContext, testProtectorName, goodCallback)
+ p, err := CreateProtector(testContext, testProtectorName, goodCallback, nil)
if err != nil {
t.Error(err)
} else {
@@ -54,7 +54,7 @@ func TestCreateProtector(t *testing.T) {
// Tests that a failure in the callback is relayed back to the caller.
func TestBadCallback(t *testing.T) {
- p, err := CreateProtector(testContext, testProtectorName, badCallback)
+ p, err := CreateProtector(testContext, testProtectorName, badCallback, nil)
if err == nil {
p.Lock()
p.Destroy()
diff --git a/actions/recovery.go b/actions/recovery.go
new file mode 100644
index 0000000..3000be6
--- /dev/null
+++ b/actions/recovery.go
@@ -0,0 +1,134 @@
+/*
+ * recovery.go - support for generating recovery passphrases
+ *
+ * Copyright 2019 Google LLC
+ *
+ * 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 actions
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+
+ "golang.org/x/sys/unix"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/google/fscrypt/crypto"
+ "github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/util"
+)
+
+// modifiedContextWithSource returns a copy of ctx with the protector source
+// replaced by source.
+func modifiedContextWithSource(ctx *Context, source metadata.SourceType) *Context {
+ modifiedConfig := proto.Clone(ctx.Config).(*metadata.Config)
+ modifiedConfig.Source = source
+ modifiedCtx := *ctx
+ modifiedCtx.Config = modifiedConfig
+ return &modifiedCtx
+}
+
+// AddRecoveryPassphrase randomly generates a recovery passphrase and adds it as
+// a custom_passphrase protector for the given Policy.
+func AddRecoveryPassphrase(policy *Policy, dirname string) (*crypto.Key, *Protector, error) {
+ // 20 random characters in a-z is 94 bits of entropy, which is way more
+ // than enough for a passphrase which still goes through the usual
+ // passphrase hashing which makes it extremely costly to brute force.
+ passphrase, err := crypto.NewRandomPassphrase(20)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer func() {
+ if err != nil {
+ passphrase.Wipe()
+ }
+ }()
+ getPassphraseFn := func(info ProtectorInfo, retry bool) (*crypto.Key, error) {
+ // CreateProtector() wipes the passphrase, but in this case we
+ // still need it for later, so make a copy.
+ return passphrase.Clone()
+ }
+ var recoveryProtector *Protector
+ customCtx := modifiedContextWithSource(policy.Context, metadata.SourceType_custom_passphrase)
+ seq := 1
+ for {
+ // Automatically generate a name for the recovery protector.
+ name := "Recovery passphrase for " + dirname
+ if seq != 1 {
+ name += " (" + strconv.Itoa(seq) + ")"
+ }
+ recoveryProtector, err = CreateProtector(customCtx, name, getPassphraseFn, policy.ownerIfCreating)
+ if err == nil {
+ break
+ }
+ if _, ok := err.(*ErrProtectorNameExists); !ok {
+ return nil, nil, err
+ }
+ seq++
+ }
+ if err := policy.AddProtector(recoveryProtector); err != nil {
+ recoveryProtector.Revert()
+ return nil, nil, err
+ }
+ return passphrase, recoveryProtector, nil
+}
+
+// WriteRecoveryInstructions writes a recovery passphrase and instructions to a
+// file. This file should initially be located in the encrypted directory
+// protected by the passphrase itself. It's up to the user to store the
+// passphrase in a different location if they actually need it.
+func WriteRecoveryInstructions(recoveryPassphrase *crypto.Key, recoveryProtector *Protector,
+ policy *Policy, path string) error {
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL|unix.O_NOFOLLOW, 0600)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ str := fmt.Sprintf(
+ `fscrypt automatically generated a recovery passphrase for this directory:
+
+ %s
+
+It did this because you chose to protect this directory with your login
+passphrase, but this directory is not on the root filesystem.
+
+Copy this passphrase to a safe place if you want to still be able to unlock this
+directory if you re-install the operating system or connect this storage media
+to a different system (which would result in your login protector being lost).
+
+To unlock this directory using this recovery passphrase, run 'fscrypt unlock'
+and select the protector named %q.
+
+If you want to disable recovery passphrase generation (not recommended),
+re-create this directory and pass the --no-recovery option to 'fscrypt encrypt'.
+Alternatively, you can remove this recovery passphrase protector using:
+
+ fscrypt metadata remove-protector-from-policy --force --protector=%s:%s --policy=%s:%s
+
+It is safe to keep it around though, as the recovery passphrase is high-entropy.
+`, recoveryPassphrase.Data(), recoveryProtector.data.Name,
+ recoveryProtector.Context.Mount.Path, recoveryProtector.data.ProtectorDescriptor,
+ policy.Context.Mount.Path, policy.data.KeyDescriptor)
+ if _, err = file.WriteString(str); err != nil {
+ return err
+ }
+ if recoveryProtector.ownerIfCreating != nil {
+ if err = util.Chown(file, recoveryProtector.ownerIfCreating); err != nil {
+ return err
+ }
+ }
+ return file.Sync()
+}
diff --git a/actions/recovery_test.go b/actions/recovery_test.go
new file mode 100644
index 0000000..35ade0e
--- /dev/null
+++ b/actions/recovery_test.go
@@ -0,0 +1,90 @@
+/*
+ * recovery_test.go - tests for recovery passphrases
+ *
+ * Copyright 2019 Google LLC
+ *
+ * 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 actions
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/fscrypt/crypto"
+)
+
+func TestRecoveryPassphrase(t *testing.T) {
+ tempDir, err := os.MkdirTemp("", "fscrypt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+ recoveryFile := filepath.Join(tempDir, "recovery.txt")
+
+ firstProtector, policy, err := makeBoth()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cleanupPolicy(policy)
+ defer cleanupProtector(firstProtector)
+
+ // Add a recovery passphrase and verify that it worked correctly.
+ passphrase, recoveryProtector, err := AddRecoveryPassphrase(policy, "foo")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cleanupProtector(recoveryProtector)
+ if passphrase.Len() != 20 {
+ t.Error("Recovery passphrase has wrong length")
+ }
+ if recoveryProtector.data.Name != "Recovery passphrase for foo" {
+ t.Error("Recovery passphrase protector has wrong name")
+ }
+ if len(policy.ProtectorDescriptors()) != 2 {
+ t.Error("There should be 2 protectors now")
+ }
+ getPassphraseFn := func(info ProtectorInfo, retry bool) (*crypto.Key, error) {
+ return passphrase.Clone()
+ }
+ recoveryProtector.Lock()
+ if err = recoveryProtector.Unlock(getPassphraseFn); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test writing the recovery instructions.
+ if err = WriteRecoveryInstructions(passphrase, recoveryProtector, policy,
+ recoveryFile); err != nil {
+ t.Fatal(err)
+ }
+ contentsBytes, err := os.ReadFile(recoveryFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ contents := string(contentsBytes)
+ if !strings.Contains(contents, string(passphrase.Data())) {
+ t.Error("Recovery instructions don't actually contain the passphrase!")
+ }
+
+ // Test for protector naming collision.
+ if passphrase, recoveryProtector, err = AddRecoveryPassphrase(policy, "foo"); err != nil {
+ t.Fatal(err)
+ }
+ defer cleanupProtector(recoveryProtector)
+ if recoveryProtector.data.Name != "Recovery passphrase for foo (2)" {
+ t.Error("Recovery passphrase protector has wrong name (after naming collision)")
+ }
+}