diff options
| -rw-r--r-- | actions/config.go | 229 | ||||
| -rw-r--r-- | actions/config_test.go | 105 | ||||
| -rw-r--r-- | actions/context.go | 97 | ||||
| -rw-r--r-- | actions/context_test.go | 76 |
4 files changed, 507 insertions, 0 deletions
diff --git a/actions/config.go b/actions/config.go new file mode 100644 index 0000000..4319814 --- /dev/null +++ b/actions/config.go @@ -0,0 +1,229 @@ +/* + * config.go - Actions for creating a new config file, which includes new + * hashing costs and the config file's location. + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package actions + +import ( + "bytes" + "log" + "os" + "runtime" + "time" + + "golang.org/x/sys/unix" + + "fscrypt/crypto" + "fscrypt/metadata" + "fscrypt/util" +) + +const ( + // LegacyConfig indicates that keys should be inserted into the keyring + // with the legacy service prefixes. Needed for kernels before v4.8. + LegacyConfig = "legacy" + // 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 +) + +var ( + // ConfigFileLocation is the location of fscrypt's global settings. + ConfigFileLocation = "/etc/fscrypt.conf" + timingPassphrase = []byte("I am a fake passphrase") + timingSalt = bytes.Repeat([]byte{42}, metadata.SaltLen) +) + +// NewConfigFile 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 NewConfigFile(target time.Duration, useLegacy bool) 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) + switch { + case os.IsExist(err): + return ErrConfigFileExists + case err != nil: + return util.UnderlyingError(err) + } + defer configFile.Close() + + config := &metadata.Config{ + Source: metadata.DefaultSource, + Options: metadata.DefaultOptions, + } + if useLegacy { + config.Compatibility = LegacyConfig + log.Printf("Using %q compatibility option\n", LegacyConfig) + } + + if config.HashCosts, err = getHashingCosts(target); err != nil { + return err + } + + log.Printf("Creating config at %q with %v\n", ConfigFileLocation, config) + return metadata.WriteConfig(config, configFile) +} + +// getConfig returns the current configuration struct. Any fields not specified +// in the config file use the system defaults. An error is returned if the +// config file hasn't been setup with NewConfigFile yet or the config contains +// invalid data. +func getConfig() (*metadata.Config, error) { + configFile, err := os.Open(ConfigFileLocation) + switch { + case os.IsNotExist(err): + return nil, ErrNoConfigFile + case err != nil: + return nil, util.UnderlyingError(err) + } + defer configFile.Close() + + log.Printf("Reading config from %q\n", ConfigFileLocation) + config, err := metadata.ReadConfig(configFile) + if err != nil { + log.Printf("ReadConfig() = %v", err) + return nil, ErrBadConfigFile + } + + // Use system defaults if not specified + if config.Source == metadata.SourceType_default { + config.Source = metadata.DefaultSource + log.Printf("Falling back to source of %q", config.Source.String()) + } + if config.Options.Padding == 0 { + config.Options.Padding = metadata.DefaultOptions.Padding + log.Printf("Falling back to padding of %d", config.Options.Padding) + } + if config.Options.Contents == metadata.EncryptionOptions_default { + config.Options.Contents = metadata.DefaultOptions.Contents + log.Printf("Falling back to contents mode of %q", config.Options.Contents) + } + if config.Options.Filenames == metadata.EncryptionOptions_default { + config.Options.Filenames = metadata.DefaultOptions.Filenames + log.Printf("Falling back to filenames mode of %q", config.Options.Filenames) + } + + if !config.IsValid() { + return nil, ErrBadConfigFile + } + + return config, nil +} + +// getHashingCosts returns hashing costs so that hashing a password will take +// approximately the target time. This is done using the total amount of RAM, +// the number of CPUs present, and by running the passphrase hash many times. +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()) + costs := &metadata.HashingCosts{ + Time: 1, + Memory: 8 * nCPUs, + Parallelism: nCPUs, + } + + // If even the minimal costs are not fast enough, just return the + // minimal costs and log a warning. + t, err := timeHashingCosts(costs) + if err != nil { + return nil, err + } + log.Printf("Min Costs={%v}\t-> %v\n", costs, t) + + if t > target { + log.Printf("time exceeded the target of %v.\n", target) + return costs, nil + } + + // Now we start doubling the costs until we reach the target. + maxMemory := ramLimit() + for { + // Store a copy of the previous costs + costsPrev := *costs + 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) + } else { + costs.Time *= 2 + } + + // 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 + } + log.Printf("Costs={%v}\t-> %v\n", costs, t) + + // If we have reached the target time, we return a set of costs + // based on the linear interpolation between the last two times. + 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, + }, 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 { + 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 kB. + return int64(info.Totalram / 1000 / 2) +} + +// betweenCosts returns a cost between a and b. Specifically, it returns the +// floor of a + f*(b-a). This way, f=0 returns a and f=1 returns b. +func betweenCosts(a, b int64, f float64) int64 { + return a + int64(f*float64(b-a)) +} + +// timeHashingCosts runs the passphrase hash with the specified costs and +// returns the time it takes to hash the passphrase. +func timeHashingCosts(costs *metadata.HashingCosts) (time.Duration, error) { + passphrase, err := crypto.NewKeyFromReader(bytes.NewReader(timingPassphrase)) + if err != nil { + return 0, err + } + defer passphrase.Wipe() + + start := time.Now() + hash, err := crypto.PassphraseHash(passphrase, timingSalt, costs) + if err == nil { + hash.Wipe() + } + + return time.Since(start), err +} diff --git a/actions/config_test.go b/actions/config_test.go new file mode 100644 index 0000000..2b10c10 --- /dev/null +++ b/actions/config_test.go @@ -0,0 +1,105 @@ +/* + * config_test.go - tests for setting up the config file + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package actions + +import ( + "io/ioutil" + "log" + "os" + "testing" + "time" +) + +const testTime = 10 * time.Millisecond + +func init() { + // All our testing uses an alternative config file location, so we don't + // need root to run the tests + ConfigFileLocation = "fscrypt_test.conf" +} + +// Tests that we can make the config files with and without legacy settings +func TestMakeConfig(t *testing.T) { + defer os.RemoveAll(ConfigFileLocation) + + err := NewConfigFile(testTime, true) + if err != nil { + t.Error(err) + } + os.RemoveAll(ConfigFileLocation) + + err = NewConfigFile(testTime, false) + if err != nil { + t.Error(err) + } +} + +// Tests that we can find valid hashing costs for various time targets and the +// estimations are somewhat close to the targets. +func TestCostsSearch(t *testing.T) { + for _, target := range []time.Duration{ + 100 * time.Microsecond, + 1 * time.Millisecond, + 10 * time.Millisecond, + 100 * time.Millisecond, + } { + costs, err := getHashingCosts(target) + if err != nil { + t.Error(err) + } + actual, err := timeHashingCosts(costs) + if err != nil { + t.Error(err) + } + + // Timing tests are only reliable for sufficiently long targets. + if target > time.Millisecond { + if actual*2 < target { + t.Errorf("actual=%v is too small (target=%v)", actual, target) + } + if target*2 < actual { + t.Errorf("actual=%v is too big (target=%v)", actual, target) + } + } + } +} + +func benchmarkCostsSearch(b *testing.B, target time.Duration) { + // Disable logging for benchmarks + log.SetOutput(ioutil.Discard) + for i := 0; i < b.N; i++ { + _, err := getHashingCosts(target) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCostsSearch10ms(b *testing.B) { + benchmarkCostsSearch(b, 10*time.Millisecond) +} + +func BenchmarkCostsSearch100ms(b *testing.B) { + benchmarkCostsSearch(b, 100*time.Millisecond) +} + +func BenchmarkCostsSearch1s(b *testing.B) { + benchmarkCostsSearch(b, time.Second) +} diff --git a/actions/context.go b/actions/context.go new file mode 100644 index 0000000..f4a3985 --- /dev/null +++ b/actions/context.go @@ -0,0 +1,97 @@ +/* + * context.go - top-level interface to fscrypt packages + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +// Package actions is the high-level interface to the fscrypt packages. The +// functions here roughly correspond with commands for the tool in cmd/fscrypt. +// 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 +package actions + +import ( + "errors" + "fmt" + "log" + + "fscrypt/filesystem" + "fscrypt/metadata" + "fscrypt/util" +) + +// Errors relating to Config files or Config structures. +var ( + ErrNoConfigFile = fmt.Errorf("config file %q does not exist", ConfigFileLocation) + ErrBadConfigFile = fmt.Errorf("config file %q has invalid data", ConfigFileLocation) + ErrConfigFileExists = fmt.Errorf("config file %q already exists", ConfigFileLocation) + ErrBadConfig = errors.New("invalid Config structure provided") +) + +// Context contains the necessary global state to perform most of fscrypt's +// actions. It contains a config struct, which is loaded from the global config +// file, but can be edited manually. A context is specific to a filesystem, and +// all actions to add, edit, remove, and apply Protectors and Policies are done +// relative to that filesystem. +type Context struct { + Config *metadata.Config + Mount *filesystem.Mount +} + +// 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. +func NewContextFromPath(path string) (ctx *Context, err error) { + ctx = new(Context) + + if ctx.Mount, err = filesystem.FindMount(path); err != nil { + err = util.UnderlyingError(err) + return + } + + if ctx.Config, err = getConfig(); err != nil { + return + } + + log.Printf("%s is on %s filesystem %q (%s)", path, + ctx.Mount.Filesystem, ctx.Mount.Path, ctx.Mount.Device) + return +} + +// 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. +func NewContextFromMountpoint(mountpoint string) (ctx *Context, err error) { + ctx = new(Context) + + if ctx.Mount, err = filesystem.GetMount(mountpoint); err != nil { + err = util.UnderlyingError(err) + return + } + + if ctx.Config, err = getConfig(); err != nil { + return + } + + log.Printf("found %s filesystem %q (%s)", ctx.Mount.Filesystem, + ctx.Mount.Path, ctx.Mount.Device) + return +} diff --git a/actions/context_test.go b/actions/context_test.go new file mode 100644 index 0000000..671b065 --- /dev/null +++ b/actions/context_test.go @@ -0,0 +1,76 @@ +/* + * config_test.go - tests for creating new contexts + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package actions + +import ( + "os" + "testing" + + "fscrypt/filesystem" +) + +var mountpoint = os.Getenv("TEST_FILESYSTEM_ROOT") + +// Makes a context using the testing locations for the filesystem and +// configuration file. +func makeContext() (*Context, error) { + if err := NewConfigFile(testTime, true); err != nil { + return nil, err + } + + mnt := filesystem.Mount{Path: mountpoint} + if err := mnt.Setup(); err != nil { + return nil, err + } + + return NewContextFromMountpoint(mountpoint) +} + +// Cleans up the testing config file and testing filesystem data. +func cleaupContext() { + os.RemoveAll(ConfigFileLocation) + mnt := filesystem.Mount{Path: mountpoint} + mnt.RemoveAllMetadata() +} + +// Tests that we can create a context +func TestSetupContext(t *testing.T) { + _, err := makeContext() + defer cleaupContext() + if err != nil { + t.Fatal(err) + } + +} + +// Tests that we cannot create a context without a config file. +func TestNoConfigFile(t *testing.T) { + mnt := filesystem.Mount{Path: mountpoint} + if err := mnt.Setup(); err != nil { + t.Fatal(err) + } + + _, err := NewContextFromMountpoint(mountpoint) + defer cleaupContext() + + if err == nil { + t.Error("should not be able to create context without config file") + } +} |