util: fscrypt requires keys of 32 bytes

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 <ndevos@ibm.com>
This commit is contained in:
Niels de Vos 2025-01-29 10:25:47 +01:00
parent 003e0b36a6
commit d9d3858db1
2 changed files with 118 additions and 6 deletions

View File

@ -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 {
@ -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
@ -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)
@ -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)
@ -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)

View File

@ -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)
}
})
}
}