util: add SecretsMetadataKMS

This new KMS is based on the (default) SecretsKMS, but instead of using
the passphrase for all volumes, the passphrase is used to
encrypt/decrypt a Data-Encryption-Key that is stored in the metadata of
the volume.

CC: Patrick Uiterwijk <puiterwijk@redhat.com> - for encryption guidance
Signed-off-by: Niels de Vos <ndevos@redhat.com>
This commit is contained in:
Niels de Vos 2021-03-02 18:48:05 +01:00 committed by mergify[bot]
parent 2b7f078943
commit 5e63743243
3 changed files with 249 additions and 0 deletions

View File

@ -235,6 +235,8 @@ func GetKMS(tenant, kmsID string, secrets map[string]string) (EncryptionKMS, err
}
switch kmsType {
case kmsTypeSecretsMetadata:
return initSecretsMetadataKMS(kmsID, secrets)
case kmsTypeVault:
return InitVaultKMS(kmsID, kmsConfig, secrets)
case kmsTypeVaultTokens:

View File

@ -17,7 +17,15 @@ limitations under the License.
package util
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"golang.org/x/crypto/scrypt"
)
const (
@ -26,6 +34,10 @@ const (
// Default KMS type
defaultKMSType = "default"
// kmsTypeSecretsMetadata is the SecretsKMS with per-volume encryption,
// where the DEK is stored in the metadata of the volume itself.
kmsTypeSecretsMetadata = "metadata"
)
// SecretsKMS is default KMS implementation that means no KMS is in use.
@ -72,3 +84,146 @@ func (kms SecretsKMS) StoreDEK(key, value string) error {
func (kms SecretsKMS) RemoveDEK(key string) error {
return nil
}
// SecretsMetadataKMS is a KMS based on the SecretsKMS, but stores the
// Data-Encryption-Key (DEK) in the metadata of the volume.
type SecretsMetadataKMS struct {
SecretsKMS
encryptionKMSID string
}
// initSecretsMetadataKMS initializes a SecretsMetadataKMS that wraps a
// SecretsKMS, so that the passphrase from the StorageClass secrets can be used
// for encrypting/decrypting DEKs that are stored in a detached DEKStore.
func initSecretsMetadataKMS(encryptionKMSID string, secrets map[string]string) (EncryptionKMS, error) {
eKMS, err := initSecretsKMS(secrets)
if err != nil {
return nil, err
}
sKMS, ok := eKMS.(SecretsKMS)
if !ok {
return nil, fmt.Errorf("failed to convert %T to SecretsKMS", eKMS)
}
smKMS := SecretsMetadataKMS{}
smKMS.SecretsKMS = sKMS
smKMS.encryptionKMSID = encryptionKMSID
return smKMS, nil
}
// GetID is returning ID representing the SecretsMetadataKMS.
func (kms SecretsMetadataKMS) GetID() string {
return kms.encryptionKMSID
}
// Destroy frees all used resources.
func (kms SecretsMetadataKMS) Destroy() {
kms.SecretsKMS.Destroy()
}
func (kms SecretsMetadataKMS) requiresDEKStore() DEKStoreType {
return DEKStoreMetadata
}
// encryptedMetedataDEK contains the encrypted DEK and the Nonce that was used
// during encryption. This structure is stored (in JSON format) in the DEKStore
// that is linked to this KMS provider.
type encryptedMetedataDEK struct {
// DEK is the encrypted data-encryption-key for the volume.
DEK []byte `json:"dek"`
// Nonce is a random byte slice to guarantee the uniqueness of the
// encrypted DEK.
Nonce []byte `json:"nonce"`
}
// EncryptDEK encrypts the plainDEK with a key derived from the passphrase from
// the SecretsKMS and the volumeID.
// The resulting encryptedDEK contains a JSON with the encrypted DEK and the
// nonce that was used for encrypting.
func (kms SecretsMetadataKMS) EncryptDEK(volumeID, plainDEK string) (string, error) {
// use the passphrase from the SecretsKMS
passphrase, err := kms.SecretsKMS.FetchDEK(volumeID)
if err != nil {
return "", fmt.Errorf("failed to get passphrase: %w", err)
}
aead, err := generateCipher(passphrase, volumeID)
if err != nil {
return "", fmt.Errorf("failed to generate cipher: %w", err)
}
emd := encryptedMetedataDEK{}
emd.Nonce, err = generateNonce(aead.NonceSize())
if err != nil {
return "", fmt.Errorf("failed to generated nonce: %w", err)
}
emd.DEK = aead.Seal(nil, emd.Nonce, []byte(plainDEK), nil)
emdData, err := json.Marshal(&emd)
if err != nil {
return "", fmt.Errorf("failed to convert "+
"encryptedMetedataDEK to JSON: %w", err)
}
return string(emdData), nil
}
// DecryptDEK takes the JSON formatted `encryptedMetedataDEK` contents, and it
// fetches SecretsKMS passphase to decrypt the DEK.
func (kms SecretsMetadataKMS) DecryptDEK(volumeID, encryptedDEK string) (string, error) {
// use the passphrase from the SecretsKMS
passphrase, err := kms.SecretsKMS.FetchDEK(volumeID)
if err != nil {
return "", fmt.Errorf("failed to get passphrase: %w", err)
}
aead, err := generateCipher(passphrase, volumeID)
if err != nil {
return "", fmt.Errorf("failed to generate cipher: %w", err)
}
emd := encryptedMetedataDEK{}
err = json.Unmarshal([]byte(encryptedDEK), &emd)
if err != nil {
return "", fmt.Errorf("failed to convert data to "+
"encryptedMetedataDEK: %w", err)
}
dek, err := aead.Open(nil, emd.Nonce, emd.DEK, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt DEK: %w", err)
}
return string(dek), nil
}
// generateCipher returns a AEAD cipher based on a passphrase and salt
// (volumeID). The cipher can then be used to encrypt/decrypt the DEK.
func generateCipher(passphrase, salt string) (cipher.AEAD, error) {
key, err := scrypt.Key([]byte(passphrase), []byte(salt), 32768, 8, 1, 32)
if err != nil {
return nil, err
}
blockCipher, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(blockCipher)
if err != nil {
return nil, err
}
return aead, nil
}
// generateNonce returns a byte slice with random contents.
func generateNonce(size int) ([]byte, error) {
nonce := make([]byte, size)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return nonce, nil
}

View File

@ -0,0 +1,92 @@
/*
Copyright 2021 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 util
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateNonce(t *testing.T) {
size := 64
nonce, err := generateNonce(size)
assert.Equal(t, size, len(nonce))
assert.NoError(t, err)
}
func TestGenerateCipher(t *testing.T) {
// nolint:gosec // this passphrase is intentionally hardcoded
passphrase := "my-cool-luks-passphrase"
salt := "unique-id-for-the-volume"
aead, err := generateCipher(passphrase, salt)
assert.NoError(t, err)
assert.NotNil(t, aead)
}
func TestInitSecretsMetadataKMS(t *testing.T) {
secrets := map[string]string{}
// passphrase it not set, init should fail
kms, err := initSecretsMetadataKMS("secrets-metadata-unit-test", secrets)
assert.Error(t, err)
assert.Nil(t, kms)
// set a passphrase to get a working KMS
secrets[encryptionPassphraseKey] = "my-passphrase-from-kubernetes"
kms, err = initSecretsMetadataKMS("secrets-metadata-unit-test", secrets)
assert.NoError(t, err)
require.NotNil(t, kms)
assert.Equal(t, "secrets-metadata-unit-test", kms.GetID())
assert.Equal(t, DEKStoreMetadata, kms.requiresDEKStore())
}
func TestWorkflowSecretsMetadataKMS(t *testing.T) {
secrets := map[string]string{
encryptionPassphraseKey: "my-passphrase-from-kubernetes",
}
volumeID := "csi-vol-1b00f5f8-b1c1-11e9-8421-9243c1f659f0"
kms, err := initSecretsMetadataKMS("secrets-metadata-unit-test", secrets)
assert.NoError(t, err)
require.NotNil(t, kms)
// plainDEK is the (LUKS) passphrase for the volume
plainDEK, err := generateNewEncryptionPassphrase()
assert.NoError(t, err)
assert.NotEqual(t, "", plainDEK)
encryptedDEK, err := kms.EncryptDEK(volumeID, plainDEK)
assert.NoError(t, err)
assert.NotEqual(t, "", encryptedDEK)
assert.NotEqual(t, plainDEK, encryptedDEK)
// with an incorrect volumeID, decrypting should fail
decryptedDEK, err := kms.DecryptDEK("incorrect-volumeID", encryptedDEK)
assert.Error(t, err)
assert.Equal(t, "", decryptedDEK)
assert.NotEqual(t, plainDEK, decryptedDEK)
// with the right volumeID, decrypting should return the plainDEK
decryptedDEK, err = kms.DecryptDEK(volumeID, encryptedDEK)
assert.NoError(t, err)
assert.NotEqual(t, "", decryptedDEK)
assert.Equal(t, plainDEK, decryptedDEK)
}