aboutsummaryrefslogtreecommitdiff
path: root/actions/config.go
diff options
context:
space:
mode:
Diffstat (limited to 'actions/config.go')
-rw-r--r--actions/config.go159
1 files changed, 120 insertions, 39 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
}