diff options
Diffstat (limited to 'actions')
| -rw-r--r-- | actions/config.go | 159 | ||||
| -rw-r--r-- | actions/config_test.go | 78 | ||||
| -rw-r--r-- | actions/context.go | 110 | ||||
| -rw-r--r-- | actions/context_test.go | 8 | ||||
| -rw-r--r-- | actions/hashing_test.go | 8 | ||||
| -rw-r--r-- | actions/policy.go | 318 | ||||
| -rw-r--r-- | actions/policy_test.go | 14 | ||||
| -rw-r--r-- | actions/protector.go | 90 | ||||
| -rw-r--r-- | actions/protector_test.go | 4 | ||||
| -rw-r--r-- | actions/recovery.go | 134 | ||||
| -rw-r--r-- | actions/recovery_test.go | 90 |
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)") + } +} |