From 462d166d5355d33a05271d24de4d52f30dd62f67 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sun, 15 Dec 2019 19:31:39 -0800 Subject: Add keyring package In preparation for introducing support for the new filesystem-level keyrings, move the existing user keyring management code from security/keyring.go and crypto/crypto.go into a new package, 'keyring'. This package provides functions AddEncryptionKey, RemoveEncryptionKey, and GetEncryptionKeyStatus which delegate to either the filesystem keyring (added by a later patch) or to the user keyring. This provides a common interface to both types of keyrings, to the extent possible. --- crypto/crypto.go | 4 ++-- crypto/crypto_test.go | 54 ++++--------------------------------------------- crypto/key.go | 56 +++++++++++++++++++-------------------------------- 3 files changed, 27 insertions(+), 87 deletions(-) (limited to 'crypto') diff --git a/crypto/crypto.go b/crypto/crypto.go index 8de8134..ec961b6 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -167,7 +167,7 @@ func Unwrap(wrappingKey *Key, data *metadata.WrappedKeyData) (*Key, error) { return nil, ErrBadAuth } - secretKey, err := newBlankKey(len(data.EncryptedKey)) + secretKey, err := NewBlankKey(len(data.EncryptedKey)) if err != nil { return nil, err } @@ -196,7 +196,7 @@ func PassphraseHash(passphrase *Key, salt []byte, costs *metadata.HashingCosts) p := uint8(costs.Parallelism) key := argon2.IDKey(passphrase.data, salt, t, m, p, metadata.InternalKeyLen) - hash, err := newBlankKey(metadata.InternalKeyLen) + hash, err := NewBlankKey(metadata.InternalKeyLen) if err != nil { return nil, err } diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 6f973ef..d0cef82 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -30,11 +30,7 @@ import ( "os" "testing" - "golang.org/x/sys/unix" - "github.com/google/fscrypt/metadata" - "github.com/google/fscrypt/security" - "github.com/google/fscrypt/util" ) // Reader that always returns the same byte @@ -53,16 +49,11 @@ func makeKey(b byte, n int) (*Key, error) { } var ( - fakeValidDescriptor = "0123456789abcdef" - fakeSalt = bytes.Repeat([]byte{'a'}, metadata.SaltLen) - fakePassword = []byte("password") - defaultService = unix.FSCRYPT_KEY_DESC_PREFIX - - fakeValidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen) - fakeInvalidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen-1) - fakeWrappingKey, _ = makeKey(17, metadata.InternalKeyLen) + fakeSalt = bytes.Repeat([]byte{'a'}, metadata.SaltLen) + fakePassword = []byte("password") - testUser, _ = util.EffectiveUser() + fakeValidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen) + fakeWrappingKey, _ = makeKey(17, metadata.InternalKeyLen) ) // As the passphrase hashing function clears the passphrase, we need to make @@ -242,43 +233,6 @@ func TestKeyLargeResize(t *testing.T) { } } -// Adds and removes a key with various services. -func TestAddRemoveKeys(t *testing.T) { - for _, service := range []string{defaultService, "ext4:", "f2fs:"} { - validDescription := service + fakeValidDescriptor - if err := InsertPolicyKey(fakeValidPolicyKey, validDescription, testUser); err != nil { - t.Error(err) - } - if err := security.RemoveKey(validDescription, testUser); err != nil { - t.Error(err) - } - } -} - -// Adds a key twice (both should succeed) -func TestAddTwice(t *testing.T) { - validDescription := defaultService + fakeValidDescriptor - InsertPolicyKey(fakeValidPolicyKey, validDescription, testUser) - if InsertPolicyKey(fakeValidPolicyKey, validDescription, testUser) != nil { - t.Error("InsertPolicyKey should not fail if key already exists") - } - security.RemoveKey(validDescription, testUser) -} - -// Makes sure a key fails with bad policy or service -func TestBadAddKeys(t *testing.T) { - validDescription := defaultService + fakeValidDescriptor - if InsertPolicyKey(fakeInvalidPolicyKey, validDescription, testUser) == nil { - security.RemoveKey(validDescription, testUser) - t.Error("InsertPolicyKey should fail with bad policy key") - } - invalidDescription := "ext4" + fakeValidDescriptor - if InsertPolicyKey(fakeValidPolicyKey, invalidDescription, testUser) == nil { - security.RemoveKey(invalidDescription, testUser) - t.Error("InsertPolicyKey should fail with bad service") - } -} - // Check that we can create random keys. All this test does to test the // "randomness" is generate a page of random bytes and attempts compression. // If the data can be compressed it is probably not very random. This isn't diff --git a/crypto/key.go b/crypto/key.go index 52efb54..2220652 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -33,7 +33,6 @@ import ( "io" "log" "os" - "os/user" "runtime" "unsafe" @@ -41,7 +40,6 @@ import ( "golang.org/x/sys/unix" "github.com/google/fscrypt/metadata" - "github.com/google/fscrypt/security" "github.com/google/fscrypt/util" ) @@ -94,9 +92,9 @@ type Key struct { data []byte } -// newBlankKey constructs a blank key of a specified length and returns an error +// NewBlankKey constructs a blank key of a specified length and returns an error // if we are unable to allocate or lock the necessary memory. -func newBlankKey(length int) (*Key, error) { +func NewBlankKey(length int) (*Key, error) { if length == 0 { return &Key{data: nil}, nil } else if length < 0 { @@ -167,7 +165,7 @@ func (key *Key) resize(requestedSize int) (*Key, error) { } defer key.Wipe() - resizedKey, err := newBlankKey(requestedSize) + resizedKey, err := NewBlankKey(requestedSize) if err != nil { return nil, err } @@ -175,6 +173,18 @@ func (key *Key) resize(requestedSize int) (*Key, error) { return resizedKey, nil } +// Data returns a slice of the key's underlying data. Note that this may become +// outdated if the key is resized. +func (key *Key) Data() []byte { + return key.data +} + +// UnsafePtr returns an unsafe pointer to the key's underlying data. Note that +// this will only be valid as long as the key is not resized. +func (key *Key) UnsafePtr() unsafe.Pointer { + return util.Ptr(key.data) +} + // UnsafeToCString makes a copy of the string's data into a null-terminated C // string allocated by C. Note that this method is unsafe as this C copy has no // locking or wiping functionality. The key shouldn't contain any `\0` bytes. @@ -190,7 +200,7 @@ func (key *Key) UnsafeToCString() unsafe.Pointer { // ensure that this original copy is secured. func NewKeyFromCString(str unsafe.Pointer) (*Key, error) { size := C.strlen((*C.char)(str)) - key, err := newBlankKey(int(size)) + key, err := NewBlankKey(int(size)) if err != nil { return nil, err } @@ -203,7 +213,7 @@ func NewKeyFromCString(str unsafe.Pointer) (*Key, error) { func NewKeyFromReader(reader io.Reader) (*Key, error) { // Use an initial key size of a page. As Mmap allocates a page anyway, // there isn't much additional overhead from starting with a whole page. - key, err := newBlankKey(os.Getpagesize()) + key, err := NewBlankKey(os.Getpagesize()) if err != nil { return nil, err } @@ -235,7 +245,7 @@ func NewKeyFromReader(reader io.Reader) (*Key, error) { // NewFixedLengthKeyFromReader constructs a key with a specified length by // reading exactly length bytes from reader. func NewFixedLengthKeyFromReader(reader io.Reader, length int) (*Key, error) { - key, err := newBlankKey(length) + key, err := NewBlankKey(length) if err != nil { return nil, err } @@ -246,30 +256,6 @@ func NewFixedLengthKeyFromReader(reader io.Reader, length int) (*Key, error) { return key, nil } -// InsertPolicyKey puts the provided policy key into the kernel keyring with the -// provided description, and type logon. The key must be a policy key. -func InsertPolicyKey(key *Key, description string, targetUser *user.User) error { - if err := util.CheckValidLength(metadata.PolicyKeyLen, key.Len()); err != nil { - return errors.Wrap(err, "policy key") - } - - // Create our payload (containing an FscryptKey) - payload, err := newBlankKey(int(unsafe.Sizeof(unix.FscryptKey{}))) - if err != nil { - return err - } - defer payload.Wipe() - - // Cast the payload to an FscryptKey so we can initialize the fields. - fscryptKey := (*unix.FscryptKey)(util.Ptr(payload.data)) - // Mode is ignored by the kernel - fscryptKey.Mode = 0 - fscryptKey.Size = metadata.PolicyKeyLen - copy(fscryptKey.Raw[:], key.data) - - return security.InsertKey(payload.data, description, targetUser) -} - var ( // The recovery code is base32 with a dash between each block of 8 characters. encoding = base32.StdEncoding @@ -290,7 +276,7 @@ func WriteRecoveryCode(key *Key, writer io.Writer) error { } // We store the base32 encoded data (without separators) in a temp key - encodedKey, err := newBlankKey(encodedLength) + encodedKey, err := NewBlankKey(encodedLength) if err != nil { return err } @@ -318,7 +304,7 @@ func WriteRecoveryCode(key *Key, writer io.Writer) error { // be given the same level of protection as a raw cryptographic key. func ReadRecoveryCode(reader io.Reader) (*Key, error) { // We store the base32 encoded data (without separators) in a temp key - encodedKey, err := newBlankKey(encodedLength) + encodedKey, err := NewBlankKey(encodedLength) if err != nil { return nil, err } @@ -347,7 +333,7 @@ func ReadRecoveryCode(reader io.Reader) (*Key, error) { } // Now we decode the key, resizing if necessary - decodedKey, err := newBlankKey(decodedLength) + decodedKey, err := NewBlankKey(decodedLength) if err != nil { return nil, err } -- cgit v1.2.3 From 2b25de6d445faefc28629603dd754aec9f744e60 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Sun, 15 Dec 2019 19:31:39 -0800 Subject: Metadata support for v2 encryption policies Linux v5.4 and later supports v2 encryption policies. These have several advantages over v1 encryption policies: - Their encryption keys can be added/removed to/from the filesystem by non-root users, thus gaining the benefits of the filesystem keyring while also retaining support for non-root use. - They use a more standard, secure, and flexible key derivation function. Because of this, some future kernel-level fscrypt features will be implemented for v2 policies only. - They prevent a denial-of-service attack where a user could associate the wrong key with another user's encrypted files. Prepare the fscrypt tool to support v2 encryption policies by: - Adding a policy_version field to the EncryptionOptions, i.e. to the config file and to the policy metadata files. - Using the kernel-specified algorithm to compute the key descriptor for v2 policies. - Handling setting and getting v2 policies. Actually adding/removing the keys for v2 policies to/from the kernel is left for the next patch. --- crypto/crypto.go | 39 +++++++++++++++++++++++++++++++++------ crypto/crypto_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) (limited to 'crypto') diff --git a/crypto/crypto.go b/crypto/crypto.go index ec961b6..9a138d0 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -28,7 +28,7 @@ // - key stretching (SHA256-based HKDF) // - key wrapping/unwrapping (Encrypt then MAC) // - passphrase-based key derivation (Argon2id) -// - descriptor computation (double SHA512) +// - key descriptor computation (double SHA512, or HKDF-SHA512) package crypto import ( @@ -38,6 +38,7 @@ import ( "crypto/sha256" "crypto/sha512" "encoding/hex" + "io" "github.com/pkg/errors" "golang.org/x/crypto/argon2" @@ -176,16 +177,42 @@ func Unwrap(wrappingKey *Key, data *metadata.WrappedKeyData) (*Key, error) { return secretKey, nil } -// ComputeDescriptor computes the descriptor for a given cryptographic key. In -// keeping with the process used in e4crypt, this uses the initial bytes -// (formatted as hexadecimal) of the double application of SHA512 on the key. -func ComputeDescriptor(key *Key) string { +func computeKeyDescriptorV1(key *Key) string { h1 := sha512.Sum512(key.data) h2 := sha512.Sum512(h1[:]) - length := hex.DecodedLen(metadata.DescriptorLen) + length := hex.DecodedLen(metadata.PolicyDescriptorLenV1) return hex.EncodeToString(h2[:length]) } +func computeKeyDescriptorV2(key *Key) (string, error) { + // This algorithm is specified by the kernel. It uses unsalted + // HKDF-SHA512, where the application-information string is the prefix + // "fscrypt\0" followed by the HKDF_CONTEXT_KEY_IDENTIFIER byte. + hkdf := hkdf.New(sha512.New, key.data, nil, []byte("fscrypt\x00\x01")) + h := make([]byte, hex.DecodedLen(metadata.PolicyDescriptorLenV2)) + if _, err := io.ReadFull(hkdf, h); err != nil { + return "", err + } + return hex.EncodeToString(h), nil +} + +// ComputeKeyDescriptor computes the descriptor for a given cryptographic key. +// If policyVersion=1, it uses the first 8 bytes of the double application of +// SHA512 on the key. Use this for protectors and v1 policy keys. +// If policyVersion=2, it uses HKDF-SHA512 to compute a key identifier that's +// compatible with the kernel's key identifiers for v2 policy keys. +// In both cases, the resulting bytes are formatted as hex. +func ComputeKeyDescriptor(key *Key, policyVersion int64) (string, error) { + switch policyVersion { + case 1: + return computeKeyDescriptorV1(key), nil + case 2: + return computeKeyDescriptorV2(key) + default: + return "", errors.Errorf("policy version of %d is invalid", policyVersion) + } +} + // PassphraseHash uses Argon2id to produce a Key given the passphrase, salt, and // hashing costs. This method is designed to take a long time and consume // considerable memory. For more information, see the documentation at diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index d0cef82..6eb0b02 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -464,6 +464,33 @@ func TestUnwrapWrongData(t *testing.T) { } } +func TestComputeKeyDescriptorV1(t *testing.T) { + descriptor, err := ComputeKeyDescriptor(fakeValidPolicyKey, 1) + if err != nil { + t.Fatal(err) + } + if descriptor != "8290608a029c5aae" { + t.Errorf("wrong v1 descriptor: %s", descriptor) + } +} + +func TestComputeKeyDescriptorV2(t *testing.T) { + descriptor, err := ComputeKeyDescriptor(fakeValidPolicyKey, 2) + if err != nil { + t.Fatal(err) + } + if descriptor != "2139f52bf8386ee99845818ac7e91c4a" { + t.Errorf("wrong v2 descriptor: %s", descriptor) + } +} + +func TestComputeKeyDescriptorBadVersion(t *testing.T) { + _, err := ComputeKeyDescriptor(fakeValidPolicyKey, 0) + if err == nil { + t.Error("computing key descriptor with bad version should fail") + } +} + // Run our test cases for passphrase hashing func TestPassphraseHashing(t *testing.T) { for i, testCase := range hashTestCases { -- cgit v1.2.3