Skip to content

Commit

Permalink
util: fscrypt requires keys of 32 bytes
Browse files Browse the repository at this point in the history
It seems that fscrypt expects a key with exactly 32 bytes. In order to
use a random length key from a KMS, either repeat the key until the
length is reached, or trim the key when needed.

See: https://github.com/google/fscrypt/tree/v0.3.4#using-a-raw-key-protector
Signed-off-by: Niels de Vos <[email protected]>
  • Loading branch information
nixpanic committed Jan 29, 2025
1 parent 003e0b3 commit d9d3858
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 6 deletions.
36 changes: 30 additions & 6 deletions internal/util/fscrypt/fscrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ var policyV2Support = []util.KernelVersion{

// error values
var (
ErrBadAuth = errors.New("key authentication check failed")
ErrBadAuth = errors.New("key authentication check failed")
ErrEmptyPassphrase = errors.New("empty passphrase given")
)

func AppendEncyptedSubdirectory(dir string) string {
Expand Down Expand Up @@ -94,14 +95,35 @@ func getPassphrase(ctx context.Context, encryption util.VolumeEncryption, volID
return passphrase, nil
}

// resizePassphrase makes sure that the given passphrase will be of [size]
// bytes. In case the passphrase is shorter, it will be repeated as many times
// as needed. When a passphrase is (or becomes) longer than the requested
// [size], the passphrase in truncated.
func resizePassphrase(passphrase string, size int) (string, error) {
if passphrase == "" || size <= 0 {
return "", ErrEmptyPassphrase
}

for len(passphrase) < size {
passphrase += passphrase
}
if len(passphrase) > size {
passphrase = string([]byte(passphrase)[:size])
}

return passphrase, nil
}

// createKeyFuncFromVolumeEncryption returns an fscrypt key function returning
// encryption keys from a VolumeEncryption struct.
func createKeyFuncFromVolumeEncryption(
ctx context.Context,
encryption util.VolumeEncryption,
volID string,
keySize int,
) (func(fscryptactions.ProtectorInfo, bool) (*fscryptcrypto.Key, error), error) {
// keys must be 32 bytes, see https://github.com/google/fscrypt?tab=readme-ov-file#using-a-raw-key-protector
keySize := encryptionPassphraseSize / 2

keyFunc := func(info fscryptactions.ProtectorInfo, retry bool) (*fscryptcrypto.Key, error) {
if retry {
return nil, ErrBadAuth
Expand All @@ -112,9 +134,11 @@ func createKeyFuncFromVolumeEncryption(
return nil, err
}

if keySize < 0 {
keySize = len(passphrase)
passphrase, err = resizePassphrase(passphrase, keySize)
if err != nil {
return nil, err
}

key, err := fscryptcrypto.NewBlankKey(keySize)
copy(key.Data(), passphrase)

Expand Down Expand Up @@ -171,7 +195,7 @@ func unlockExisting(
errMsg := fmt.Sprintf("fscrypt: unlock with protector error: %v", err)
log.ErrorLog(ctx, "%s, retry using a null padded passphrase", errMsg)

keyFn, err = createKeyFuncFromVolumeEncryption(ctx, *volEncryption, volID, encryptionPassphraseSize/2)
keyFn, err = createKeyFuncFromVolumeEncryption(ctx, *volEncryption, volID)
if err != nil {
log.ErrorLog(ctx, "fscrypt: could not create key function: %v", err)

Expand Down Expand Up @@ -376,7 +400,7 @@ func Unlock(
stagingTargetPath string, volID string,
) error {
// Fetches keys from KMS. Do this first to catch KMS errors before setting up anything.
keyFn, err := createKeyFuncFromVolumeEncryption(ctx, *volEncryption, volID, -1)
keyFn, err := createKeyFuncFromVolumeEncryption(ctx, *volEncryption, volID)
if err != nil {
log.ErrorLog(ctx, "fscrypt: could not create key function: %v", err)

Expand Down
88 changes: 88 additions & 0 deletions internal/util/fscrypt/fscrypt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2025 The Ceph-CSI Authors.
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 fscrypt

import (
"errors"
"testing"
)

func TestResizePassphrase(t *testing.T) {
t.Parallel()
tests := []struct {
name string
passphrase string
size int
ret string
err error
}{
{
"matching passphrase size",
"secret",
6,
"secret",
nil,
},
{
"short passphrase",
"secret",
64,
"secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecr",
nil,
},
{
"long passphrase",
"secret",
2,
"se",
nil,
},
{
"empty passphrase",
"",
16,
"",
ErrEmptyPassphrase,
},
{
"zero length requested",
"secret",
0,
"",
ErrEmptyPassphrase,
},
{
"negative length requested",
"secret",
-32,
"",
ErrEmptyPassphrase,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

ret, err := resizePassphrase(tt.passphrase, tt.size)
if ret != tt.ret {
t.Errorf("resizePassphrase() returned %q of %d bytes, expected %q of %d bytes", tt.ret, len(tt.ret), ret, len(ret))
}
if !errors.Is(err, tt.err) {
t.Errorf("resizePassphrase() returned %v as error, expected %v", err, tt.err)
}
})
}
}

0 comments on commit d9d3858

Please sign in to comment.