aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions/callback.go8
-rw-r--r--actions/config.go33
-rw-r--r--actions/context.go63
-rw-r--r--actions/context_test.go82
-rw-r--r--actions/hashing_test.go (renamed from actions/config_test.go)45
-rw-r--r--actions/policy.go334
-rw-r--r--actions/policy_test.go76
-rw-r--r--actions/protector.go125
-rw-r--r--actions/protector_test.go25
-rw-r--r--filesystem/mountpoint.go6
10 files changed, 424 insertions, 373 deletions
diff --git a/actions/callback.go b/actions/callback.go
index 18be670..2415d8c 100644
--- a/actions/callback.go
+++ b/actions/callback.go
@@ -21,10 +21,13 @@
package actions
import (
+ "log"
+
+ "github.com/pkg/errors"
+
"fscrypt/crypto"
"fscrypt/filesystem"
"fscrypt/metadata"
- "log"
)
// ProtectorInfo is the information a caller will receive about a Protector
@@ -91,7 +94,8 @@ func unwrapProtectorKey(info ProtectorInfo, keyFn KeyFunc) (*crypto.Key, error)
protectorKey, err := crypto.Unwrap(wrappingKey, info.data.WrappedKey)
wrappingKey.Wipe()
- switch err {
+
+ switch errors.Cause(err) {
case nil:
log.Printf("valid wrapping key for protector %s", info.Descriptor())
return protectorKey, nil
diff --git a/actions/config.go b/actions/config.go
index 2010ef1..1d81ff9 100644
--- a/actions/config.go
+++ b/actions/config.go
@@ -27,6 +27,8 @@ import (
"runtime"
"time"
+ "github.com/pkg/errors"
+
"golang.org/x/sys/unix"
"fscrypt/crypto"
@@ -68,7 +70,7 @@ func CreateConfigFile(target time.Duration, useLegacy bool) error {
case os.IsExist(err):
return ErrConfigFileExists
case err != nil:
- return util.UnderlyingError(err)
+ return err
}
defer configFile.Close()
@@ -99,15 +101,14 @@ func getConfig() (*metadata.Config, error) {
case os.IsNotExist(err):
return nil, ErrNoConfigFile
case err != nil:
- return nil, util.UnderlyingError(err)
+ return nil, 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
+ return nil, errors.Wrap(ErrBadConfigFile, err.Error())
}
// Use system defaults if not specified
@@ -128,8 +129,8 @@ func getConfig() (*metadata.Config, error) {
log.Printf("Falling back to filenames mode of %q", config.Options.Filenames)
}
- if !config.IsValid() {
- return nil, ErrBadConfigFile
+ if err := config.CheckValidity(); err != nil {
+ return nil, errors.Wrap(ErrBadConfigFile, err.Error())
}
return config, nil
@@ -203,8 +204,8 @@ func ramLimit() int64 {
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)
+ // Use half the RAM and convert to kiB.
+ return int64(info.Totalram / 1024 / 2)
}
// betweenCosts returns a cost between a and b. Specifically, it returns the
@@ -222,11 +223,23 @@ func timeHashingCosts(costs *metadata.HashingCosts) (time.Duration, error) {
}
defer passphrase.Wipe()
- start := time.Now()
+ // Be sure to measure CPU time, not wall time (time.Now)
+ begin := cpuTimeInNanoseconds()
hash, err := crypto.PassphraseHash(passphrase, timingSalt, costs)
if err == nil {
hash.Wipe()
}
+ end := cpuTimeInNanoseconds()
- return time.Since(start), err
+ return time.Duration(end - begin), nil
+}
+
+// cpuTimeInNanoseconds returns the nanosecond count based on the process's CPU usage.
+// This number has no absolute meaning, only relative meaning to other calls.
+func cpuTimeInNanoseconds() int64 {
+ var ts unix.Timespec
+ err := unix.ClockGettime(unix.CLOCK_PROCESS_CPUTIME_ID, &ts)
+ // ClockGettime fails if given a bad address or on a VERY old system.
+ util.NeverError(err)
+ return unix.TimespecToNsec(ts)
}
diff --git a/actions/context.go b/actions/context.go
index 4d7d30d..f8d0a3d 100644
--- a/actions/context.go
+++ b/actions/context.go
@@ -29,21 +29,22 @@
package actions
import (
- "errors"
- "fmt"
"log"
+ "github.com/pkg/errors"
+
+ "fscrypt/crypto"
"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)
+ 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("method needs a call to Unlock() first")
)
// Context contains the necessary global state to perform most of fscrypt's
@@ -61,12 +62,9 @@ type Context struct {
// 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
}
@@ -81,12 +79,9 @@ func NewContextFromPath(path string) (ctx *Context, err error) {
// 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
}
@@ -99,15 +94,29 @@ func NewContextFromMountpoint(mountpoint string) (ctx *Context, err error) {
// checkContext verifies that the context contains an valid config and a mount
// which is being used with fscrypt.
func (ctx *Context) checkContext() error {
- if !ctx.Config.IsValid() {
- return ErrBadConfig
+ if err := ctx.Config.CheckValidity(); err != nil {
+ return errors.Wrap(ErrBadConfig, err.Error())
}
return ctx.Mount.CheckSetup()
}
-// GetProtectorOption returns the ProtectorOption for the protector on the
+// 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
+// (which are necessary for kernels before v4.8).
+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 + ":"
+ }
+ }
+ return crypto.DefaultService
+}
+
+// getProtectorOption returns the ProtectorOption for the protector on the
// context's mountpoint with the specified descriptor.
-func (ctx *Context) GetProtectorOption(protectorDescriptor string) *ProtectorOption {
+func (ctx *Context) getProtectorOption(protectorDescriptor string) *ProtectorOption {
mnt, data, err := ctx.Mount.GetProtector(protectorDescriptor)
if err != nil {
return &ProtectorOption{ProtectorInfo{}, nil, err}
@@ -121,9 +130,12 @@ func (ctx *Context) GetProtectorOption(protectorDescriptor string) *ProtectorOpt
return &ProtectorOption{info, mnt, nil}
}
-// ListProtectorOptions creates a slice of all the options for all of the
-// Protectors on the Context's mountpoint.
-func (ctx *Context) ListProtectorOptions() ([]*ProtectorOption, error) {
+// ProtectorOptions creates a slice of all the options for all of the Protectors
+// on the Context's mountpoint.
+func (ctx *Context) ProtectorOptions() ([]*ProtectorOption, error) {
+ if err := ctx.checkContext(); err != nil {
+ return nil, err
+ }
descriptors, err := ctx.Mount.ListProtectors()
if err != nil {
return nil, err
@@ -131,18 +143,7 @@ func (ctx *Context) ListProtectorOptions() ([]*ProtectorOption, error) {
options := make([]*ProtectorOption, len(descriptors))
for i, descriptor := range descriptors {
- options[i] = ctx.GetProtectorOption(descriptor)
+ options[i] = ctx.getProtectorOption(descriptor)
}
return options, nil
}
-
-// ListOptionsForPolicy creates a slice of the ProtectorOptions which protect
-// the policy specified by policyDescriptor.
-func (ctx *Context) ListOptionsForPolicy(policyDescriptor string) ([]*ProtectorOption, error) {
- policy, err := getPolicyData(ctx, policyDescriptor)
- if err != nil {
- return nil, err
- }
-
- return policy.listOptions(), nil
-}
diff --git a/actions/context_test.go b/actions/context_test.go
index 74629a3..79adedf 100644
--- a/actions/context_test.go
+++ b/actions/context_test.go
@@ -20,57 +20,77 @@
package actions
import (
+ "fmt"
+ "fscrypt/util"
+ "log"
"os"
+ "path/filepath"
"testing"
-
- "fscrypt/filesystem"
+ "time"
)
-var mountpoint = os.Getenv("TEST_FILESYSTEM_ROOT")
+const testTime = 10 * time.Millisecond
+
+// holds the context we will use throughout the actions tests
+var testContext *Context
// Makes a context using the testing locations for the filesystem and
// configuration file.
-func makeContext() (*Context, error) {
- if err := CreateConfigFile(testTime, true); err != nil {
+func setupContext() (ctx *Context, err error) {
+ mountpoint, err := util.TestPath()
+ if err != nil {
return nil, err
}
- mnt := filesystem.Mount{Path: mountpoint}
- if err := mnt.Setup(); err != nil {
- return nil, err
- }
+ ConfigFileLocation = filepath.Join(mountpoint, "test.conf")
- return NewContextFromMountpoint(mountpoint)
-}
+ // Should not be able to setup without a config file
+ if badCtx, badCtxErr := NewContextFromMountpoint(mountpoint); badCtxErr == nil {
+ badCtx.Mount.RemoveAllMetadata()
+ return nil, fmt.Errorf("created context at %q without config file", badCtx.Mount.Path)
+ }
-// Cleans up the testing config file and testing filesystem data.
-func cleaupContext() {
- os.RemoveAll(ConfigFileLocation)
- mnt := filesystem.Mount{Path: mountpoint}
- mnt.RemoveAllMetadata()
-}
+ if err = CreateConfigFile(testTime, true); err != nil {
+ return nil, err
+ }
+ defer func() {
+ if err != nil {
+ os.RemoveAll(ConfigFileLocation)
+ }
+ }()
-// Tests that we can create a context
-func TestSetupContext(t *testing.T) {
- _, err := makeContext()
- defer cleaupContext()
+ ctx, err = NewContextFromMountpoint(mountpoint)
if err != nil {
- t.Fatal(err)
+ return nil, err
}
+ return ctx, ctx.Mount.Setup()
}
-// 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)
+// Cleans up the testing config file and testing filesystem data.
+func cleaupContext(ctx *Context) error {
+ err1 := os.RemoveAll(ConfigFileLocation)
+ err2 := ctx.Mount.RemoveAllMetadata()
+ if err1 != nil {
+ return err1
}
+ return err2
+}
- _, err := NewContextFromMountpoint(mountpoint)
- defer cleaupContext()
+func TestMain(m *testing.M) {
+ log.SetFlags(log.LstdFlags | log.Lmicroseconds)
+ var err error
+ testContext, err = setupContext()
+ if err != nil {
+ fmt.Printf("setupContext() = %v\n", err)
+ os.Exit(1)
+ }
- if err == nil {
- t.Error("should not be able to create context without config file")
+ returnCode := m.Run()
+ err = cleaupContext(testContext)
+ if err != nil {
+ fmt.Printf("cleanupContext() = %v\n", err)
+ os.Exit(1)
}
+ os.Exit(returnCode)
}
diff --git a/actions/config_test.go b/actions/hashing_test.go
index c0b2089..e3cffb6 100644
--- a/actions/config_test.go
+++ b/actions/hashing_test.go
@@ -1,5 +1,5 @@
/*
- * config_test.go - tests for setting up the config file
+ * hashing_test.go - tests for computing and benchmarking hashing costs
*
* Copyright 2017 Google Inc.
* Author: Joe Richey (joerichey@google.com)
@@ -22,43 +22,17 @@ 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 := CreateConfigFile(testTime, true)
- if err != nil {
- t.Error(err)
- }
- os.RemoveAll(ConfigFileLocation)
-
- err = CreateConfigFile(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,
+ 20 * time.Millisecond,
100 * time.Millisecond,
+ 500 * time.Millisecond,
} {
costs, err := getHashingCosts(target)
if err != nil {
@@ -69,14 +43,11 @@ func TestCostsSearch(t *testing.T) {
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)
- }
+ 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)
}
}
}
diff --git a/actions/policy.go b/actions/policy.go
index 678bcdc..ff61e8b 100644
--- a/actions/policy.go
+++ b/actions/policy.go
@@ -20,10 +20,12 @@
package actions
import (
- "errors"
+ "fmt"
"log"
"reflect"
+ "github.com/pkg/errors"
+
"fscrypt/crypto"
"fscrypt/filesystem"
"fscrypt/metadata"
@@ -32,74 +34,14 @@ import (
// Errors relating to Policies
var (
- ErrMissingPolicyMetadata = util.SystemError("policy for directory has no filesystem metadata; metadata may be corrupted")
- ErrPolicyMetadataMismatch = util.SystemError("policy metadata is inconsistent; metadata may be corrupted")
- ErrPathWrongFilesystem = errors.New("provided path for policy is on the wrong filesystem")
+ 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("this policy is already protected by this protector")
- ErrNotProtected = errors.New("this policy is not protected by this protector")
+ ErrAlreadyProtected = errors.New("policy already protected by protector")
+ ErrNotProtected = errors.New("policy not protected by protector")
)
-// PolicyDescriptorForPath returns the policy descriptor for a file on the
-// filesystem. An error is returned if the metadata is inconsistent, the path is
-// for the wrong filesystem, or the path is not encrypted.
-func PolicyDescriptorForPath(ctx *Context, path string) (string, error) {
- if err := ctx.checkContext(); err != nil {
- return "", err
- }
- // Policies and their paths will always be on the same filesystem
- if pathMount, err := filesystem.FindMount(path); err != nil {
- return "", err
- } else if pathMount != ctx.Mount {
- return "", ErrPathWrongFilesystem
- }
- log.Printf("%q is on mountpoint %q", path, ctx.Mount.Path)
-
- // 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)
- if err != nil {
- return "", err
- }
- descriptor := pathData.KeyDescriptor
- log.Printf("found policy %s for %q", descriptor, path)
-
- mountData, err := ctx.Mount.GetPolicy(descriptor)
- if err != nil {
- log.Printf("getting metadata for policy %s: %v", descriptor, err)
- return "", ErrMissingPolicyMetadata
- }
- 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 "", ErrPolicyMetadataMismatch
- }
- log.Print("data from filesystem and path agree")
-
- return descriptor, nil
-}
-
-// IsPolicyUnlocked returns a boolean indicating if the corresponding policy for
-// this filesystem has its key in the keyring, meaning files and directories
-// using this policy can be read and written.
-func IsPolicyUnlocked(ctx *Context, policyDescriptor string) bool {
- _, err := crypto.FindPolicyKey(policyDescriptor, getService(ctx))
- return err == nil
-}
-
-// LockPolicy removes a policy key from the keyring. This means after unmounting
-// and remounting the directory, files and directories using this policy will be
-// inaccessible.
-func LockPolicy(ctx *Context, policyDescriptor string) error {
- if err := ctx.checkContext(); err != nil {
- return err
- }
- return crypto.RemovePolicyKey(policyDescriptor, getService(ctx))
-}
-
// 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.
@@ -112,46 +54,21 @@ func PurgeAllPolicies(ctx *Context) error {
return err
}
- for _, policy := range policies {
- if err := LockPolicy(ctx, policy); err == crypto.ErrKeyringDelete {
- // This means a policy key was present but we could not
- // delete it. The other errors just indicate that the
- // policy key was not present.
+ for _, policyDescriptor := range policies {
+ service := ctx.getService()
+ err = crypto.RemovePolicyKey(policyDescriptor, service)
+
+ switch errors.Cause(err) {
+ case nil, crypto.ErrKeyringSearch:
+ // We don't care if the key has already been removed
+ break
+ default:
return err
}
}
return nil
}
-// 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
-// (which are necessary for kernels before v4.8).
-func getService(ctx *Context) string {
- if ctx.Config.HasCompatibilityOption(LegacyConfig) {
- switch ctx.Mount.Filesystem {
- case "ext4", "f2fs":
- return ctx.Mount.Filesystem + ":"
- }
- }
- return crypto.DefaultService
-}
-
-// getPolicyData creates a partially constructed policy by looking up
-// the descriptor on the appropriate filesystem. The policy returned will not
-// have its key initialized.
-func getPolicyData(ctx *Context, descriptor string) (*Policy, error) {
- if err := ctx.checkContext(); err != nil {
- return nil, err
- }
- data, err := ctx.Mount.GetPolicy(descriptor)
- if err != nil {
- return nil, err
- }
- log.Printf("got data for %s from %q", descriptor, ctx.Mount.Path)
-
- return &Policy{Context: ctx, data: data}, nil
-}
-
// Policy represents an unlocked policy, so it contains the PolicyData as well
// as the actual protector key. These unlocked Polices can then be applied to a
// directory, or have their key material inserted into the keyring (which will
@@ -161,6 +78,7 @@ type Policy struct {
Context *Context
data *metadata.PolicyData
key *crypto.Key
+ created bool
}
// CreatePolicy creates a Policy protected by given Protector and stores the
@@ -182,51 +100,127 @@ func CreatePolicy(ctx *Context, protector *Protector) (*Policy, error) {
Options: ctx.Config.Options,
KeyDescriptor: crypto.ComputeDescriptor(key),
},
- key: key,
+ key: key,
+ created: true,
}
if err = policy.AddProtector(protector); err != nil {
- policy.Wipe()
+ policy.Lock()
return nil, err
}
return policy, nil
}
-// GetPolicy retrieves a policy with a specific descriptor. As a Protector is
-// needed to unlock the policy, callbacks to select the policy and get the key
-// are needed. This method will retry the keyFn as necessary to get the correct
-// key for the selected protector.
-func GetPolicy(ctx *Context, descriptor string, optionFn OptionFunc, keyFn KeyFunc) (*Policy, error) {
- policy, err := getPolicyData(ctx, descriptor)
+// GetPolicy retrieves a locked policy with a specific descriptor. The Policy is
+// still locked in this case, so it must be unlocked before using certain
+// methods.
+func GetPolicy(ctx *Context, descriptor string) (*Policy, error) {
+ if err := ctx.checkContext(); err != nil {
+ return nil, err
+ }
+ data, err := ctx.Mount.GetPolicy(descriptor)
+ if err != nil {
+ return nil, err
+ }
+ log.Printf("got data for %s from %q", descriptor, ctx.Mount.Path)
+
+ return &Policy{Context: ctx, data: data}, nil
+}
+
+// GetPolicyFromPath returns the locked policy descriptor for a file on the
+// filesystem. The Policy is still locked in this case, so it must be unlocked
+// before using certain methods. An error is returned if the metadata is
+// inconsistent or the path is not encrypted.
+func GetPolicyFromPath(ctx *Context, path string) (*Policy, error) {
+ if err := ctx.checkContext(); err != nil {
+ return nil, err
+ }
+
+ // 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)
if err != nil {
return nil, err
}
- return policy, policy.unwrapPolicy(optionFn, keyFn)
+ descriptor := pathData.KeyDescriptor
+ log.Printf("found policy %s for %q", descriptor, path)
+
+ mountData, err := ctx.Mount.GetPolicy(descriptor)
+ if err != nil {
+ log.Printf("getting policy metadata: %v", err)
+ return nil, errors.Wrap(ErrMissingPolicyMetadata, path)
+ }
+ 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)
+ }
+ log.Print("data from filesystem and path agree")
+
+ return &Policy{Context: ctx, data: mountData}, nil
}
-// listOptions creates a slice of ProtectorOptions for the protectors protecting
-// this policy.
-func (policy *Policy) listOptions() []*ProtectorOption {
+// ProtectorOptions creates a slice of ProtectorOptions for the protectors
+// protecting this policy.
+func (policy *Policy) ProtectorOptions() []*ProtectorOption {
options := make([]*ProtectorOption, len(policy.data.WrappedPolicyKeys))
for i, wrappedPolicyKey := range policy.data.WrappedPolicyKeys {
- options[i] = policy.Context.GetProtectorOption(wrappedPolicyKey.ProtectorDescriptor)
+ options[i] = policy.Context.getProtectorOption(wrappedPolicyKey.ProtectorDescriptor)
}
return options
}
-// unwrapPolicy initializes the policy key using the provided callbacks.
-func (policy *Policy) unwrapPolicy(optionFn OptionFunc, keyFn KeyFunc) error {
- // Create a list of the ProtectorOptions and a list of the wrapped keys.
- options := policy.listOptions()
- wrappedKeys := make([]*metadata.WrappedKeyData, len(policy.data.WrappedPolicyKeys))
-
+// ProtectorDescriptors creates a slice of the Protector descriptors for the
+// protectors protecting this policy.
+func (policy *Policy) ProtectorDescriptors() []string {
+ descriptors := make([]string, len(policy.data.WrappedPolicyKeys))
for i, wrappedPolicyKey := range policy.data.WrappedPolicyKeys {
- wrappedKeys[i] = wrappedPolicyKey.WrappedKey
+ descriptors[i] = wrappedPolicyKey.ProtectorDescriptor
}
+ return descriptors
+}
+
+// Descriptor returns the key descriptor for this policy.
+func (policy *Policy) Descriptor() string {
+ return policy.data.KeyDescriptor
+}
+
+// Destroy removes a policy from the filesystem. The internal key should still
+// be wiped with Lock().
+func (policy *Policy) Destroy() error {
+ return policy.Context.Mount.RemovePolicy(policy.Descriptor())
+}
+
+// Revert destroys a policy if it was created, but does nothing if it was just
+// queried from the filesystem.
+func (policy *Policy) Revert() error {
+ if !policy.created {
+ return nil
+ }
+ return policy.Destroy()
+}
+
+func (policy *Policy) String() string {
+ return fmt.Sprintf("Policy: %s\nMountpoint: %s\nOptions: %v\nProtectors:%+v",
+ policy.Descriptor(), policy.Context.Mount, policy.data.Options,
+ policy.ProtectorDescriptors())
+}
+
+// Unlock unwraps the Policy's internal key. As a Protector is needed to unlock
+// the Policy, callbacks to select the Policy and get the key are needed. This
+// method will retry the keyFn as necessary to get the correct key for the
+// selected protector. Does nothing if policy is already unlocked.
+func (policy *Policy) Unlock(optionFn OptionFunc, keyFn KeyFunc) error {
+ if policy.key != nil {
+ return nil
+ }
+ options := policy.ProtectorOptions()
// The OptionFunc indicates which option and wrapped key we should use.
- idx, err := optionFn(policy.data.KeyDescriptor, options)
+ idx, err := optionFn(policy.Descriptor(), options)
if err != nil {
return err
}
@@ -235,52 +229,67 @@ func (policy *Policy) unwrapPolicy(optionFn OptionFunc, keyFn KeyFunc) error {
return option.LoadError
}
- wrappedPolicyKey := wrappedKeys[idx]
log.Printf("protector %s selected in callback", option.Descriptor())
-
protectorKey, err := unwrapProtectorKey(option.ProtectorInfo, keyFn)
if err != nil {
return err
}
defer protectorKey.Wipe()
- log.Printf("unwrapping policy %s with protector", policy.data.KeyDescriptor)
+ log.Printf("unwrapping policy %s with protector", policy.Descriptor())
+ wrappedPolicyKey := policy.data.WrappedPolicyKeys[idx].WrappedKey
policy.key, err = crypto.Unwrap(protectorKey, wrappedPolicyKey)
return err
}
+// Lock wipes a Policy's internal Key. It should always be called after using a
+// Policy. This is often done with a defer statement. There is no effect if
+// called multiple times.
+func (policy *Policy) Lock() error {
+ err := policy.key.Wipe()
+ policy.key = nil
+ return err
+}
+
// 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
-// different filesystems, a link will be created between them.
+// different filesystems, a link will be created between them. The policy and
+// protector must both be unlocked.
func (policy *Policy) AddProtector(protector *Protector) error {
- _, err := policy.findWrappedKeyIndex(protector.data.ProtectorDescriptor)
- if err == nil {
+ if _, ok := policy.findWrappedKeyIndex(protector.Descriptor()); ok {
return ErrAlreadyProtected
}
+ if policy.key == nil || protector.key == nil {
+ return ErrLocked
+ }
// 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 {
- err = policy.Context.Mount.AddLinkedProtector(
- protector.data.ProtectorDescriptor, 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)
if err != nil {
return err
}
+ } else {
+ log.Printf("policy and protector both on %q", policy.Context.Mount)
}
// Create the wrapped policy key
- wrappedPolicyKey := &metadata.WrappedPolicyKey{
- ProtectorDescriptor: protector.data.ProtectorDescriptor,
- }
- if wrappedPolicyKey.WrappedKey, err = crypto.Wrap(protector.key, policy.key); err != nil {
+ wrappedKey, err := crypto.Wrap(protector.key, policy.key)
+ if err != nil {
return err
}
// Append the wrapped key to the data
- policy.addKey(wrappedPolicyKey)
+ policy.addKey(&metadata.WrappedPolicyKey{
+ ProtectorDescriptor: protector.Descriptor(),
+ WrappedKey: wrappedKey,
+ })
- if err = policy.commitData(); err != nil {
+ if err := policy.commitData(); err != nil {
// revert the addition on failure
policy.removeKey(len(policy.data.WrappedPolicyKeys) - 1)
return err
@@ -290,13 +299,13 @@ 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 w do not attempt to remove
-// any links (for the case where the protector and policy are on different
-// filesystems). This is because one protector may protect many polices.
-func (policy *Policy) RemoveProtector(protectorDescriptor string) error {
- idx, err := policy.findWrappedKeyIndex(protectorDescriptor)
- if err != nil {
- return err
+// is returned, no data has been changed. Note that no protector links are
+// 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())
+ if !ok {
+ return ErrNotProtected
}
if len(policy.data.WrappedPolicyKeys) == 1 {
@@ -306,7 +315,7 @@ func (policy *Policy) RemoveProtector(protectorDescriptor string) error {
// Remove the wrapped key from the data
toRemove := policy.removeKey(idx)
- if err = policy.commitData(); err != nil {
+ if err := policy.commitData(); err != nil {
// revert the removal on failure (order is irrelevant)
policy.addKey(toRemove)
return err
@@ -327,40 +336,43 @@ func (policy *Policy) Apply(path string) error {
return metadata.SetPolicy(path, policy.data)
}
-// Unlock provisions the Policy key into the kernel keyring. This allows reading
-// and writing of files encrypted with this directory.
-func (policy *Policy) Unlock() error {
- return crypto.InsertPolicyKey(policy.key, policy.data.KeyDescriptor, getService(policy.Context))
+// 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 := crypto.FindPolicyKey(policy.Descriptor(), policy.Context.getService())
+ return err == nil
}
-// Wipe wipes a Policy's internal Key. It should always be called after using a
-// Policy. This is often done with a defer statement.
-func (policy *Policy) Wipe() error {
- return policy.key.Wipe()
+// Provision inserts the Policy key into the kernel keyring. This allows reading
+// and writing of files encrypted with this directory. Requires unlocked Policy.
+func (policy *Policy) Provision() error {
+ if policy.key == nil {
+ return ErrLocked
+ }
+ return crypto.InsertPolicyKey(policy.key, policy.Descriptor(), policy.Context.getService())
}
-// Destroy removes a policy from the filesystem. The internal key should still
-// be wiped with Wipe().
-func (policy *Policy) Destroy() error {
- return policy.Context.Mount.RemovePolicy(policy.data.KeyDescriptor)
+// 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 crypto.RemovePolicyKey(policy.Descriptor(), policy.Context.getService())
}
-// commitData writes the Policy's current data to the filesystem
+// commitData writes the Policy's current data to the filesystem.
func (policy *Policy) commitData() error {
return policy.Context.Mount.AddPolicy(policy.data)
}
// findWrappedPolicyKey returns the index of the wrapped policy key
-// corresponding to this policy and protector. An error is returned if no
-// wrapped policy key corresponds to the specified protector.
-func (policy *Policy) findWrappedKeyIndex(protectorDescriptor string) (int, error) {
+// corresponding to this policy and protector. The returned bool is false if no
+// wrapped policy key corresponds to the specified protector, true otherwise.
+func (policy *Policy) findWrappedKeyIndex(protectorDescriptor string) (int, bool) {
for idx, wrappedPolicyKey := range policy.data.WrappedPolicyKeys {
if wrappedPolicyKey.ProtectorDescriptor == protectorDescriptor {
- return idx, nil
+ return idx, true
}
}
-
- return 0, ErrNotProtected
+ return 0, false
}
// addKey adds the wrapped policy key to end of the wrapped key data.
diff --git a/actions/policy_test.go b/actions/policy_test.go
index 07a7f87..96b9bb0 100644
--- a/actions/policy_test.go
+++ b/actions/policy_test.go
@@ -21,53 +21,54 @@ package actions
import "testing"
-// Makes a context, protector, and policy
-func makeAll() (ctx *Context, protector *Protector, policy *Policy, err error) {
- ctx, err = makeContext()
+// Makes a protector and policy
+func makeBoth() (*Protector, *Policy, error) {
+ protector, err := CreateProtector(testContext, testProtectorName, goodCallback)
if err != nil {
- return
+ return nil, nil, err
}
- protector, err = CreateProtector(ctx, testProtectorName, goodCallback)
+ policy, err := CreatePolicy(testContext, protector)
if err != nil {
- return
+ cleanupProtector(protector)
+ return nil, nil, err
}
- policy, err = CreatePolicy(ctx, protector)
- return
+ return protector, policy, nil
}
-// Cleans up a context, protector, and policy
-func cleanupAll(protector *Protector, policy *Policy) {
- if policy != nil {
- policy.Wipe()
- }
- if protector != nil {
- protector.Wipe()
- }
- cleaupContext()
+func cleanupProtector(protector *Protector) {
+ protector.Lock()
+ protector.Destroy()
+}
+
+func cleanupPolicy(policy *Policy) {
+ policy.Lock()
+ policy.Destroy()
}
// Tests that we can make a policy/protector pair
func TestCreatePolicy(t *testing.T) {
- _, pro, pol, err := makeAll()
- defer cleanupAll(pro, pol)
+ pro, pol, err := makeBoth()
if err != nil {
t.Error(err)
}
+ cleanupPolicy(pol)
+ cleanupProtector(pro)
}
// Tests that we can add another protector to the policy
func TestPolicyGoodAddProtector(t *testing.T) {
- ctx, pro1, pol, err := makeAll()
- defer cleanupAll(pro1, pol)
+ pro1, pol, err := makeBoth()
+ defer cleanupProtector(pro1)
+ defer cleanupPolicy(pol)
if err != nil {
t.Fatal(err)
}
- pro2, err := CreateProtector(ctx, testProtectorName2, goodCallback)
+ pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback)
if err != nil {
t.Fatal(err)
}
- defer pro2.Wipe()
+ defer cleanupProtector(pro2)
err = pol.AddProtector(pro2)
if err != nil {
@@ -77,8 +78,9 @@ func TestPolicyGoodAddProtector(t *testing.T) {
// Tests that we cannot add a protector to a policy twice
func TestPolicyBadAddProtector(t *testing.T) {
- _, pro, pol, err := makeAll()
- defer cleanupAll(pro, pol)
+ pro, pol, err := makeBoth()
+ defer cleanupProtector(pro)
+ defer cleanupPolicy(pol)
if err != nil {
t.Fatal(err)
}
@@ -90,24 +92,25 @@ func TestPolicyBadAddProtector(t *testing.T) {
// Tests that we can remove a protector we added
func TestPolicyGoodRemoveProtector(t *testing.T) {
- ctx, pro1, pol, err := makeAll()
- defer cleanupAll(pro1, pol)
+ pro1, pol, err := makeBoth()
+ defer cleanupProtector(pro1)
+ defer cleanupPolicy(pol)
if err != nil {
t.Fatal(err)
}
- pro2, err := CreateProtector(ctx, testProtectorName2, goodCallback)
+ pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback)
if err != nil {
t.Fatal(err)
}
- defer pro2.Wipe()
+ defer cleanupProtector(pro2)
err = pol.AddProtector(pro2)
if err != nil {
t.Fatal(err)
}
- err = pol.RemoveProtector(pro1.data.ProtectorDescriptor)
+ err = pol.RemoveProtector(pro1)
if err != nil {
t.Error(err)
}
@@ -115,23 +118,24 @@ func TestPolicyGoodRemoveProtector(t *testing.T) {
// Tests various bad ways to remove protectors
func TestPolicyBadRemoveProtector(t *testing.T) {
- ctx, pro1, pol, err := makeAll()
- defer cleanupAll(pro1, pol)
+ pro1, pol, err := makeBoth()
+ defer cleanupProtector(pro1)
+ defer cleanupPolicy(pol)
if err != nil {
t.Fatal(err)
}
- pro2, err := CreateProtector(ctx, testProtectorName2, goodCallback)
+ pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback)
if err != nil {
t.Fatal(err)
}
- defer pro2.Wipe()
+ defer cleanupProtector(pro2)
- if pol.RemoveProtector(pro2.data.ProtectorDescriptor) == nil {
+ if pol.RemoveProtector(pro2) == nil {
t.Error("we should not be able to remove a protector we did not add")
}
- if pol.RemoveProtector(pro1.data.ProtectorDescriptor) == nil {
+ if pol.RemoveProtector(pro1) == 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 4680cba..0409b56 100644
--- a/actions/protector.go
+++ b/actions/protector.go
@@ -20,9 +20,12 @@
package actions
import (
- "errors"
+ "fmt"
+ "log"
"os"
+ "github.com/pkg/errors"
+
"fscrypt/crypto"
"fscrypt/metadata"
)
@@ -31,21 +34,21 @@ import (
var (
ErrProtectorName = errors.New("login protectors do not need a name")
ErrMissingProtectorName = errors.New("custom protectors must have a name")
- ErrDuplicateName = errors.New("a protector with this name already exists")
- ErrDuplicateUID = errors.New("there is already a login protector for this user")
+ ErrDuplicateName = errors.New("protector with this name already exists")
+ ErrDuplicateUID = errors.New("login protector for this user already exists")
)
// checkForProtectorWithName returns an error if there is already a protector
// on the filesystem with a specific name (or if we cannot read the necessary
// data).
func checkForProtectorWithName(ctx *Context, name string) error {
- options, err := ctx.ListProtectorOptions()
+ options, err := ctx.ProtectorOptions()
if err != nil {
return err
}
for _, option := range options {
if option.Name() == name {
- return ErrDuplicateName
+ return errors.Wrapf(ErrDuplicateName, "name %q", name)
}
}
return nil
@@ -55,13 +58,13 @@ func checkForProtectorWithName(ctx *Context, name string) error {
// protector on the filesystem with a specific UID (or if we cannot read the
// necessary data).
func checkForProtectorWithUID(ctx *Context, uid int64) error {
- options, err := ctx.ListProtectorOptions()
+ options, err := ctx.ProtectorOptions()
if err != nil {
return err
}
for _, option := range options {
if option.Source() == metadata.SourceType_pam_passphrase && option.UID() == uid {
- return ErrDuplicateUID
+ return errors.Wrapf(ErrDuplicateUID, "uid %d", uid)
}
}
return nil
@@ -75,12 +78,13 @@ type Protector struct {
Context *Context
data *metadata.ProtectorData
key *crypto.Key
+ created bool
}
-// CreateProtector creates a protector with a given name (only 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.
+// 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) {
if err := ctx.checkContext(); err != nil {
return nil, err
@@ -109,6 +113,7 @@ func CreateProtector(ctx *Context, name string, keyFn KeyFunc) (*Protector, erro
Name: name,
Source: ctx.Config.Source,
},
+ created: true,
}
// Extra data is needed for some SourceTypes
@@ -138,36 +143,34 @@ func CreateProtector(ctx *Context, name string, keyFn KeyFunc) (*Protector, erro
protector.data.ProtectorDescriptor = crypto.ComputeDescriptor(protector.key)
if err := protector.Rewrap(keyFn); err != nil {
- protector.Wipe()
+ protector.Lock()
return nil, err
}
return protector, nil
}
-// GetProtector retrieves a Protector with a specific descriptor. The keyFn
-// provided to unwrap the Protector key will be retied as necessary to get the
-// correct key.
-func GetProtector(ctx *Context, descriptor string, keyFn KeyFunc) (*Protector, error) {
- if err := ctx.checkContext(); err != nil {
- return nil, err
- }
- var err error
- protector := &Protector{Context: ctx}
-
- if protector.data, err = ctx.Mount.GetRegularProtector(descriptor); err != nil {
+// GetProtector retrieves a Protector with a specific descriptor. The Protector
+// is still locked in this case, so it must be unlocked before using certain
+// methods.
+func GetProtector(ctx *Context, descriptor string) (*Protector, error) {
+ log.Printf("Getting protector %s", descriptor)
+ err := ctx.checkContext()
+ if err != nil {
return nil, err
}
- protector.key, err = unwrapProtectorKey(ProtectorInfo{protector.data}, keyFn)
+ protector := &Protector{Context: ctx}
+ protector.data, err = ctx.Mount.GetRegularProtector(descriptor)
return protector, err
}
// GetProtectorFromOption retrieves a protector based on a protector option.
// If the option had a load error, this function returns that error. The
-// keyFn provided to unwrap the Protector key will be retied as necessary to
-// get the correct key.
-func GetProtectorFromOption(ctx *Context, option *ProtectorOption, keyFn KeyFunc) (*Protector, error) {
+// Protector is still locked in this case, so it must be unlocked before using
+// certain methods.
+func GetProtectorFromOption(ctx *Context, option *ProtectorOption) (*Protector, error) {
+ log.Printf("Getting protector %s from option", option.Descriptor())
if err := ctx.checkContext(); err != nil {
return nil, err
}
@@ -179,18 +182,62 @@ func GetProtectorFromOption(ctx *Context, option *ProtectorOption, keyFn KeyFunc
if option.LinkedMount != nil {
ctx = &Context{ctx.Config, option.LinkedMount}
}
- var err error
- protector := &Protector{Context: ctx, data: option.data}
+ return &Protector{Context: ctx, data: option.data}, nil
+}
- protector.key, err = unwrapProtectorKey(option.ProtectorInfo, keyFn)
- return protector, err
+// Descriptor returns the protector descriptor.
+func (protector *Protector) Descriptor() string {
+ return protector.data.ProtectorDescriptor
+}
+
+// Destroy removes a protector from the filesystem. The internal key should
+// still be wiped with Lock().
+func (protector *Protector) Destroy() error {
+ return protector.Context.Mount.RemoveProtector(protector.Descriptor())
+}
+
+// Revert destroys a protector if it was created, but does nothing if it was
+// just queried from the filesystem.
+func (protector *Protector) Revert() error {
+ if !protector.created {
+ return nil
+ }
+ return protector.Destroy()
+}
+
+func (protector *Protector) String() string {
+ return fmt.Sprintf("Protector: %s\nMountpoint: %s\nSource: %s\nName: %s\nCosts: %v\nUID: %d",
+ protector.Descriptor(), protector.Context.Mount, protector.data.Source,
+ protector.data.Name, protector.data.Costs, protector.data.Uid)
+}
+
+// Unlock unwraps the Protector's internal key. The keyFn provided to unwrap the
+// Protector key will be retried as necessary to get the correct key. Lock()
+// should be called after use. Does nothing if protector is already unlocked.
+func (protector *Protector) Unlock(keyFn KeyFunc) (err error) {
+ if protector.key != nil {
+ return
+ }
+ protector.key, err = unwrapProtectorKey(ProtectorInfo{protector.data}, keyFn)
+ return
+}
+
+// Lock wipes a Protector's internal Key. It should always be called after using
+// an unlocked Protector. This is often done with a defer statement. There is
+// no effect if called multiple times.
+func (protector *Protector) Lock() error {
+ err := protector.key.Wipe()
+ protector.key = nil
+ return err
}
// Rewrap updates the data that is wrapping the Protector Key. This is useful if
// a user's password has changed, for example. The keyFn provided to rewrap
-// the Protector key will only be called once. If an error is returned, no data
-// has been changed on the filesystem.
+// the Protector key will only be called once. Requires unlocked Protector.
func (protector *Protector) Rewrap(keyFn KeyFunc) error {
+ if protector.key == nil {
+ return ErrLocked
+ }
wrappingKey, err := getWrappingKey(ProtectorInfo{protector.data}, keyFn, false)
if err != nil {
return err
@@ -211,15 +258,3 @@ func (protector *Protector) Rewrap(keyFn KeyFunc) error {
return protector.Context.Mount.AddProtector(protector.data)
}
-
-// Wipe wipes a Protector's internal Key. It should always be called after using
-// a Protector. This is often done with a defer statement.
-func (protector *Protector) Wipe() error {
- return protector.key.Wipe()
-}
-
-// Destroy removes a protector from the filesystem. The internal key should
-// still be wiped with Wipe().
-func (protector *Protector) Destroy() error {
- return protector.Context.Mount.RemoveProtector(protector.data.ProtectorDescriptor)
-}
diff --git a/actions/protector_test.go b/actions/protector_test.go
index 08d9aed..eacba83 100644
--- a/actions/protector_test.go
+++ b/actions/protector_test.go
@@ -21,9 +21,10 @@ package actions
import (
"bytes"
- "errors"
"testing"
+ "github.com/pkg/errors"
+
. "fscrypt/crypto"
)
@@ -42,31 +43,21 @@ func badCallback(info ProtectorInfo, retry bool) (*Key, error) {
// Tests that we can create a valid protector.
func TestCreateProtector(t *testing.T) {
- ctx, err := makeContext()
- defer cleaupContext()
- if err != nil {
- t.Fatal(err)
- }
-
- p, err := CreateProtector(ctx, testProtectorName, goodCallback)
+ p, err := CreateProtector(testContext, testProtectorName, goodCallback)
if err != nil {
t.Error(err)
} else {
- p.Wipe()
+ p.Lock()
+ p.Destroy()
}
}
// Tests that a failure in the callback is relayed back to the caller.
func TestBadCallback(t *testing.T) {
- ctx, err := makeContext()
- defer cleaupContext()
- if err != nil {
- t.Fatal(err)
- }
-
- p, err := CreateProtector(ctx, testProtectorName, badCallback)
+ p, err := CreateProtector(testContext, testProtectorName, badCallback)
if err == nil {
- p.Wipe()
+ p.Lock()
+ p.Destroy()
}
if err != errCallback {
t.Error("callback error was not relayed back to caller")
diff --git a/filesystem/mountpoint.go b/filesystem/mountpoint.go
index 1fc41be..eab7592 100644
--- a/filesystem/mountpoint.go
+++ b/filesystem/mountpoint.go
@@ -139,9 +139,9 @@ func AllFilesystems() ([]*Mount, error) {
return nil, err
}
- mounts := make([]*Mount, len(mountsByPath))
- for i, mount := range mountsByPath {
- mounts[i] = mount
+ mounts := make([]*Mount, 0, len(mountsByPath))
+ for _, mount := range mountsByPath {
+ mounts = append(mounts, mount)
}
sort.Sort(PathSorter(mounts))