]> git.apps.os.sepia.ceph.com Git - fscrypt.git/commitdiff
actions: generate a config file for fscrypt
authorJoe Richey joerichey@google.com <joerichey@google.com>
Wed, 24 May 2017 01:57:13 +0000 (18:57 -0700)
committerJoe Richey joerichey@google.com <joerichey@google.com>
Wed, 31 May 2017 19:41:30 +0000 (12:41 -0700)
This commit adds in the actions package. This package will be the
highest-level interface to the fscrypt packages. The public functions
in this package will be called directly from cmd/fscrypt.

The actions added in this commit pertain to creating and reading the
fscrypt global config file "fscrypt.conf". The challenging part about
creating this file is finding the correct hashing parameters for the
desired time target.

The getHashingCosts() function finds the desired costs by doubling the
costs and running the passphrase hash until the target is exceeded.
Then, a cost estimate is obtained using a linear interpolation between
the last two costs (and their time results).

Change-Id: I4a0eaf4856ec4ff49eb4360da3267f7caa9d07b2

actions/config.go [new file with mode: 0644]
actions/config_test.go [new file with mode: 0644]
actions/context.go [new file with mode: 0644]
actions/context_test.go [new file with mode: 0644]

diff --git a/actions/config.go b/actions/config.go
new file mode 100644 (file)
index 0000000..4319814
--- /dev/null
@@ -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 (file)
index 0000000..2b10c10
--- /dev/null
@@ -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 (file)
index 0000000..f4a3985
--- /dev/null
@@ -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 (file)
index 0000000..671b065
--- /dev/null
@@ -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")
+       }
+}