--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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")
+ }
+}