aboutsummaryrefslogtreecommitdiff
path: root/crypto
diff options
context:
space:
mode:
authorebiggers <ebiggers@google.com>2020-01-22 18:28:23 -0800
committerGitHub <noreply@github.com>2020-01-22 18:28:23 -0800
commit059482129c5fdafebc582887a4ae4ef80988b708 (patch)
tree8ec373c41a677ff6949148b56f4aeaafe22791a6 /crypto
parent80654f23ebfd552277ed217a2c5e1d0bb1374189 (diff)
parentfe2939cc7e50f4c6025253efdf7380c04fac9ae1 (diff)
Merge pull request #148 from ebiggers/fscrypt-key-mgmt-improvements
Filesystem keyring and v2 encryption policy support
Diffstat (limited to 'crypto')
-rw-r--r--crypto/crypto.go43
-rw-r--r--crypto/crypto_test.go81
-rw-r--r--crypto/key.go56
3 files changed, 87 insertions, 93 deletions
diff --git a/crypto/crypto.go b/crypto/crypto.go
index 8de8134..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"
@@ -167,7 +168,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
}
@@ -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
@@ -196,7 +223,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..6eb0b02 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
@@ -510,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 {
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
}