Secrets: Refactor data_key_id out of the encoded secure value payload (#111852)

* everything compiles

* tests pass

* remove file included by accident

* add entry to gitignore

* some scaffolding for the migration executor

* remove file

* implement and test the migration

* use xkube.Namespace in our interfaces

* add todo

* update wire deps

* add some logs

* fix wire dependency ordering

* create tests to validate error conditions during migrations
This commit is contained in:
Michael Mandrus
2025-10-03 15:25:46 -04:00
committed by GitHub
parent 0c0c66fda1
commit acad92864e
49 changed files with 782 additions and 232 deletions
@@ -3,6 +3,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} (
{{ .Ident "name" }},
{{ .Ident "version" }},
{{ .Ident "encrypted_data" }},
{{ .Ident "data_key_id" }},
{{ .Ident "created" }},
{{ .Ident "updated" }}
) VALUES (
@@ -10,6 +11,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} (
{{ .Arg .Row.Name }},
{{ .Arg .Row.Version }},
{{ .Arg .Row.EncryptedData }},
{{ .Arg .Row.DataKeyID }},
{{ .Arg .Row.Created }},
{{ .Arg .Row.Updated }}
);
@@ -3,6 +3,7 @@ SELECT
{{ .Ident "name" }},
{{ .Ident "version" }},
{{ .Ident "encrypted_data" }},
{{ .Ident "data_key_id" }},
{{ .Ident "created" }},
{{ .Ident "updated" }}
FROM
@@ -3,6 +3,7 @@ SELECT
{{ .Ident "name" }},
{{ .Ident "version" }},
{{ .Ident "encrypted_data" }},
{{ .Ident "data_key_id" }},
{{ .Ident "created" }},
{{ .Ident "updated" }}
FROM
@@ -2,6 +2,7 @@ UPDATE
{{ .Ident "secret_encrypted_value" }}
SET
{{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }},
{{ .Ident "data_key_id" }} = {{ .Arg .DataKeyID }},
{{ .Ident "updated" }} = {{ .Arg .Updated }}
WHERE
{{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
@@ -6,6 +6,7 @@ type EncryptedValue struct {
Namespace string
Name string
Version int64
DataKeyID string
EncryptedData []byte
Created int64
Updated int64
@@ -1,7 +1,9 @@
package encryption
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"time"
@@ -10,6 +12,7 @@ import (
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
@@ -37,9 +40,9 @@ type encryptedValStorage struct {
tracer trace.Tracer
}
func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (ev *contracts.EncryptedValue, err error) {
func (s *encryptedValStorage) Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) (ev *contracts.EncryptedValue, err error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
))
defer span.End()
@@ -56,10 +59,11 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string
createdTime := time.Now().Unix()
encryptedValue := &EncryptedValue{
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
EncryptedData: encryptedData,
EncryptedData: encryptedData.EncryptedData,
DataKeyID: encryptedData.DataKeyID,
Created: createdTime,
Updated: createdTime,
}
@@ -88,18 +92,21 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string
}
return &contracts.EncryptedValue{
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedData: encryptedValue.EncryptedData,
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: encryptedValue.DataKeyID,
EncryptedData: encryptedValue.EncryptedData,
},
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
}, nil
}
func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error {
func (s *encryptedValStorage) Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) error {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
@@ -107,10 +114,11 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string
req := updateEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
EncryptedData: encryptedData,
EncryptedData: encryptedData.EncryptedData,
DataKeyID: encryptedData.DataKeyID,
Updated: time.Now().Unix(),
}
@@ -133,9 +141,9 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string
return nil
}
func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, version int64) (*contracts.EncryptedValue, error) {
func (s *encryptedValStorage) Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*contracts.EncryptedValue, error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
@@ -143,7 +151,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v
req := &readEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
}
@@ -163,7 +171,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v
}
var encryptedValue EncryptedValue
err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated)
err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.DataKeyID, &encryptedValue.Created, &encryptedValue.Updated)
if err != nil {
return nil, fmt.Errorf("failed to scan encrypted value row: %w", err)
}
@@ -172,18 +180,21 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v
}
return &contracts.EncryptedValue{
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedData: encryptedValue.EncryptedData,
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: encryptedValue.DataKeyID,
EncryptedData: encryptedValue.EncryptedData,
},
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
}, nil
}
func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string, version int64) error {
func (s *encryptedValStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
@@ -191,7 +202,7 @@ func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string
req := deleteEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
}
@@ -264,6 +275,7 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts.
&row.Name,
&row.Version,
&row.EncryptedData,
&row.DataKeyID,
&row.Created,
&row.Updated,
)
@@ -272,12 +284,15 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts.
}
encryptedValues = append(encryptedValues, &contracts.EncryptedValue{
Namespace: row.Namespace,
Name: row.Name,
Version: row.Version,
EncryptedData: row.EncryptedData,
Created: row.Created,
Updated: row.Updated,
Namespace: row.Namespace,
Name: row.Name,
Version: row.Version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: row.DataKeyID,
EncryptedData: row.EncryptedData,
},
Created: row.Created,
Updated: row.Updated,
})
}
if err := rows.Err(); err != nil {
@@ -329,3 +344,77 @@ func (s *globalEncryptedValStorage) CountAll(ctx context.Context, untilTime *int
return count, nil
}
type encryptedValMigrationExecutor struct {
db contracts.Database
dialect sqltemplate.Dialect
tracer trace.Tracer
encryptedValueStore contracts.EncryptedValueStorage
globalStore contracts.GlobalEncryptedValueStorage
}
func ProvideEncryptedValueMigrationExecutor(
db contracts.Database,
tracer trace.Tracer,
encryptedValueStore contracts.EncryptedValueStorage,
globalStore contracts.GlobalEncryptedValueStorage,
) (contracts.EncryptedValueMigrationExecutor, error) {
return &encryptedValMigrationExecutor{
db: db,
dialect: sqltemplate.DialectForDriver(db.DriverName()),
tracer: tracer,
encryptedValueStore: encryptedValueStore,
globalStore: globalStore,
}, nil
}
func (s *encryptedValMigrationExecutor) Execute(ctx context.Context) (int, error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueMigrationExecutor.Execute")
defer span.End()
// 1. Retrieve all encrypted values
encryptedValues, err := s.globalStore.ListAll(ctx, contracts.ListOpts{}, nil)
if err != nil {
return 0, fmt.Errorf("listing all encrypted values: %w", err)
}
// This doesn't need to be done in a single transaction because there's no risk to successful rows if other rows fail
rowsAffected := 0
for _, encryptedValue := range encryptedValues {
// 2. If the value already has the data key id broken out, skip it
if encryptedValue.DataKeyID != "" {
continue
}
// 3. Split the data key id and the encrypted data out from the encoded payload
payload := encryptedValue.EncryptedData
const keyIdDelimiter = '#'
payload = payload[1:]
endOfKey := bytes.Index(payload, []byte{keyIdDelimiter})
if endOfKey == -1 {
return rowsAffected, fmt.Errorf("could not find valid key id in encrypted payload with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version)
}
b64Key := payload[:endOfKey]
encryptedData := payload[endOfKey+1:]
if len(encryptedData) == 0 {
return rowsAffected, fmt.Errorf("encrypted data is empty with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version)
}
keyId := make([]byte, base64.RawStdEncoding.DecodedLen(len(b64Key)))
_, err := base64.RawStdEncoding.Decode(keyId, b64Key)
if err != nil {
return rowsAffected, fmt.Errorf("decoding key id with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err)
}
// 4. Update the encrypted value with the data key id and the encrypted data
err = s.encryptedValueStore.Update(ctx, xkube.Namespace(encryptedValue.Namespace), encryptedValue.Name, encryptedValue.Version, contracts.EncryptedPayload{
DataKeyID: string(keyId),
EncryptedData: encryptedData,
})
if err != nil {
return rowsAffected, fmt.Errorf("updating encrypted value with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err)
}
rowsAffected++
}
return rowsAffected, nil
}
@@ -2,15 +2,25 @@ package encryption_test
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"slices"
"testing"
"text/template"
"time"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
cipherService "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/storage/secret/encryption"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"pgregory.net/rapid"
)
@@ -21,7 +31,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data"))
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
require.NotEmpty(t, createdEV.Namespace)
require.NotEmpty(t, createdEV.Name)
@@ -36,10 +49,13 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data"))
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version)
require.NoError(t, err)
require.Equal(t, createdEV.Namespace, obtainedEV.Namespace)
@@ -47,6 +63,7 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
require.Equal(t, createdEV.Created, obtainedEV.Created)
require.Equal(t, createdEV.Updated, obtainedEV.Updated)
require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData)
require.Equal(t, createdEV.DataKeyID, obtainedEV.DataKeyID)
require.Equal(t, createdEV.Namespace, obtainedEV.Namespace)
})
@@ -54,7 +71,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, []byte("test-data"))
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "ns2", createdEV.Name, createdEV.Version)
@@ -78,16 +98,23 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data"))
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
err = sut.EncryptedValueStorage.Update(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version, []byte("test-data-updated"))
err = sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id-updated",
EncryptedData: []byte("test-data-updated"),
})
require.NoError(t, err)
updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version)
require.NoError(t, err)
require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData)
require.Equal(t, "test-data-key-id-updated", updatedEV.DataKeyID)
require.Equal(t, createdEV.Created, updatedEV.Created)
require.Equal(t, createdEV.Namespace, updatedEV.Namespace)
})
@@ -96,7 +123,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, []byte("test-data"))
err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.Error(t, err)
})
@@ -104,16 +134,19 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("ttttest-data"))
createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("ttttest-data"),
})
require.NoError(t, err)
_, err = sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
_, err = sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version)
require.NoError(t, err)
err = sut.EncryptedValueStorage.Delete(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
err = sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version)
require.NoError(t, err)
obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version)
obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version)
require.Error(t, err)
require.Nil(t, obtainedEV)
})
@@ -130,10 +163,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data"))
createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data"))
createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
// List all encrypted values, without pagination
@@ -180,10 +219,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
_, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data"))
_, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
_, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data"))
_, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{
DataKeyID: "test-data-key-id",
EncryptedData: []byte("test-data"),
})
require.NoError(t, err)
count, err := sut.GlobalEncryptedValueStorage.CountAll(t.Context(), nil)
@@ -198,6 +243,281 @@ func TestEncryptedValueStoreImpl(t *testing.T) {
})
}
func TestEncryptedValueMigration(t *testing.T) {
t.Parallel()
t.Run("golden path - successful migration of legacy format", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
tracer := noop.NewTracerProvider().Tracer("test")
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := cipherService.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
testCases := []struct {
namespace string
name string
version int64
plaintext string
dataKeyId string
}{
{
namespace: "test-namespace-1",
name: "test-name-1",
version: 1,
plaintext: "test-plaintext-1",
dataKeyId: "test-data-key-id-1",
},
{
namespace: "test-namespace-1",
name: "test-name-2",
version: 1,
plaintext: "test-plaintext-2",
dataKeyId: "test-data-key-id-1",
},
{
namespace: "test-namespace-2",
name: "test-name-3",
version: 1,
plaintext: "test-plaintext-3",
dataKeyId: "test-data-key-id-2",
},
}
// Seed with data in the legacy format
for _, tc := range testCases {
err := createLegacyEncryptedData(t, sut, enc, tc.namespace, tc.name, tc.version, tc.plaintext, tc.dataKeyId)
require.NoError(t, err)
}
// Run the migration and blindy trust it
rowsAffected, err := sut.EncryptedValueMigrationExecutor.Execute(t.Context())
require.NoError(t, err)
require.Equal(t, len(testCases), rowsAffected)
// Now validate that the data is in the new format
encryptedValues, err := sut.GlobalEncryptedValueStorage.ListAll(t.Context(), contracts.ListOpts{}, nil)
require.NoError(t, err)
require.Len(t, encryptedValues, 3)
for _, tc := range testCases {
ev, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(tc.namespace), tc.name, tc.version)
require.NoError(t, err)
// Decrypt the encrypted data and check for equality
decrypted, err := enc.Decrypt(t.Context(), ev.EncryptedData, tc.dataKeyId)
require.NoError(t, err)
require.Equal(t, tc.dataKeyId, ev.DataKeyID)
require.Equal(t, tc.plaintext, string(decrypted))
}
})
t.Run("error conditions - handles corrupt data gracefully", func(t *testing.T) {
t.Parallel()
tracer := noop.NewTracerProvider().Tracer("test")
sut := testutils.Setup(t)
t.Run("global store list error", func(t *testing.T) {
mockGlobalStore := &mockGlobalEncryptedValueStorage{
listAllError: errors.New("database connection failed"),
}
migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(
sut.Database,
tracer,
sut.EncryptedValueStorage,
mockGlobalStore,
)
require.NoError(t, err)
rowsAffected, err := migrationExecutor.Execute(t.Context())
require.Error(t, err)
require.Contains(t, err.Error(), "listing all encrypted values")
require.Equal(t, 0, rowsAffected)
})
t.Run("corrupt data - missing key delimiter", func(t *testing.T) {
mockGlobalStore := &mockGlobalEncryptedValueStorage{
encryptedValues: []*contracts.EncryptedValue{
{
Namespace: "test-ns",
Name: "test-name",
Version: 1,
EncryptedPayload: contracts.EncryptedPayload{
EncryptedData: []byte("corrupt-data-without-delimiter"),
DataKeyID: "", // Empty to trigger migration
},
},
},
}
migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(
sut.Database,
tracer,
sut.EncryptedValueStorage,
mockGlobalStore,
)
require.NoError(t, err)
rowsAffected, err := migrationExecutor.Execute(t.Context())
require.Error(t, err)
require.Contains(t, err.Error(), "could not find valid key id in encrypted payload")
require.Equal(t, 0, rowsAffected)
})
t.Run("corrupt data - empty encrypted data", func(t *testing.T) {
mockGlobalStore := &mockGlobalEncryptedValueStorage{
encryptedValues: []*contracts.EncryptedValue{
{
Namespace: "test-ns",
Name: "test-name",
Version: 1,
EncryptedPayload: contracts.EncryptedPayload{
EncryptedData: []byte("#dGVzdA#"), // Valid key but no encrypted data after delimiter
DataKeyID: "", // Empty to trigger migration
},
},
},
}
migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(
sut.Database,
tracer,
sut.EncryptedValueStorage,
mockGlobalStore,
)
require.NoError(t, err)
rowsAffected, err := migrationExecutor.Execute(t.Context())
require.Error(t, err)
require.Contains(t, err.Error(), "encrypted data is empty")
require.Equal(t, 0, rowsAffected)
})
t.Run("corrupt data - invalid base64 key", func(t *testing.T) {
mockGlobalStore := &mockGlobalEncryptedValueStorage{
encryptedValues: []*contracts.EncryptedValue{
{
Namespace: "test-ns",
Name: "test-name",
Version: 1,
EncryptedPayload: contracts.EncryptedPayload{
EncryptedData: []byte("#invalid-base64!@#$%^&*()#somedata"),
DataKeyID: "", // Empty to trigger migration
},
},
},
}
migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(
sut.Database,
tracer,
sut.EncryptedValueStorage,
mockGlobalStore,
)
require.NoError(t, err)
rowsAffected, err := migrationExecutor.Execute(t.Context())
require.Error(t, err)
require.Contains(t, err.Error(), "decoding key id")
require.Equal(t, 0, rowsAffected)
})
t.Run("update failure", func(t *testing.T) {
mockGlobalStore := &mockGlobalEncryptedValueStorage{
encryptedValues: []*contracts.EncryptedValue{
{
Namespace: "nonexistent-ns",
Name: "nonexistent-name",
Version: 999,
EncryptedPayload: contracts.EncryptedPayload{
EncryptedData: []byte("#dGVzdA#someencrypteddata"),
DataKeyID: "", // Empty to trigger migration
},
},
},
}
migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(
sut.Database,
tracer,
sut.EncryptedValueStorage,
mockGlobalStore,
)
require.NoError(t, err)
rowsAffected, err := migrationExecutor.Execute(t.Context())
require.Error(t, err)
require.Contains(t, err.Error(), "updating encrypted value")
require.Equal(t, 0, rowsAffected)
})
})
}
// Helper function that bypasses interfaces and creates data in the legacy format directly in the database.
// The format is "#{encoded_key_id}#{encrypted_data}".
func createLegacyEncryptedData(t *testing.T, sut testutils.Sut, enc cipher.Cipher, namespace, name string, version int64, plaintext string, dataKeyId string) error {
t.Helper()
encryptedData, err := enc.Encrypt(t.Context(), []byte(plaintext), dataKeyId)
require.NoError(t, err)
// Encode using the legacy format
const keyIdDelimiter = '#'
prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len(dataKeyId))+2)
base64.RawStdEncoding.Encode(prefix[1:], []byte(dataKeyId))
prefix[0] = keyIdDelimiter
prefix[len(prefix)-1] = keyIdDelimiter
blob := make([]byte, len(prefix)+len(encryptedData))
copy(blob, prefix)
copy(blob[len(prefix):], encryptedData)
createdTime := time.Now().Unix()
encryptedValue := &encryption.EncryptedValue{
Namespace: namespace,
Name: name,
Version: version,
EncryptedData: blob,
DataKeyID: "",
Created: createdTime,
Updated: createdTime,
}
req := struct {
sqltemplate.SQLTemplate
Row *encryption.EncryptedValue
}{
SQLTemplate: sqltemplate.New(sqltemplate.DialectForDriver(sut.Database.DriverName())),
Row: encryptedValue,
}
tmpl, err := template.ParseFiles("data/encrypted_value_create.sql")
if err != nil {
return fmt.Errorf("parsing template: %w", err)
}
query, err := sqltemplate.Execute(tmpl, req)
if err != nil {
return fmt.Errorf("executing template: %w", err)
}
res, err := sut.Database.ExecContext(t.Context(), query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("inserting row: %w", err)
}
if rowsAffected, err := res.RowsAffected(); err != nil {
return fmt.Errorf("getting rows affected: %w", err)
} else if rowsAffected != 1 {
return fmt.Errorf("expected 1 row affected, got %d", rowsAffected)
}
return nil
}
func TestStateMachine(t *testing.T) {
t.Parallel()
@@ -212,10 +532,14 @@ func TestStateMachine(t *testing.T) {
ns := namespaceGen.Draw(t, "ns")
name := nameGen.Draw(t, "name")
version := versionGen.Draw(t, "version")
dataKeyId := rapid.String().Draw(t, "dataKeyId")
plaintext := rapid.String().Draw(t, "plaintext")
_, modelErr := m.create(ns, name, version, []byte(plaintext))
_, err := sut.EncryptedValueStorage.Create(t.Context(), ns, name, version, []byte(plaintext))
_, modelErr := m.create(ns, name, version, []byte(plaintext), dataKeyId)
_, err := sut.EncryptedValueStorage.Create(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{
DataKeyID: dataKeyId,
EncryptedData: []byte(plaintext),
})
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -225,10 +549,14 @@ func TestStateMachine(t *testing.T) {
ns := namespaceGen.Draw(t, "ns")
name := nameGen.Draw(t, "name")
version := versionGen.Draw(t, "version")
dataKeyId := rapid.String().Draw(t, "dataKeyId")
plaintext := rapid.String().Draw(t, "plaintext")
modelErr := m.update(ns, name, version, []byte(plaintext))
err := sut.EncryptedValueStorage.Update(t.Context(), ns, name, version, []byte(plaintext))
modelErr := m.update(ns, name, version, []byte(plaintext), dataKeyId)
err := sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{
DataKeyID: dataKeyId,
EncryptedData: []byte(plaintext),
})
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -240,7 +568,7 @@ func TestStateMachine(t *testing.T) {
version := versionGen.Draw(t, "version")
modelValue, modelErr := m.get(ns, name, version)
value, err := sut.EncryptedValueStorage.Get(t.Context(), ns, name, version)
value, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(ns), name, version)
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -258,7 +586,7 @@ func TestStateMachine(t *testing.T) {
version := versionGen.Draw(t, "version")
modelErr := m.delete(ns, name, version)
err := sut.EncryptedValueStorage.Delete(t.Context(), ns, name, version)
err := sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(ns), name, version)
if modelErr != nil || err != nil {
require.ErrorIs(t, err, modelErr)
return
@@ -290,18 +618,19 @@ type entry struct {
name string
version int64
encryptedData []byte
dataKeyId string
}
func newModel() *model {
return &model{}
}
func (m *model) create(namespace, name string, version int64, encryptedData []byte) (*contracts.EncryptedValue, error) {
func (m *model) create(namespace, name string, version int64, encryptedData []byte, dataKeyId string) (*contracts.EncryptedValue, error) {
v, err := m.get(namespace, name, version)
if err != nil && !errors.Is(err, encryption.ErrEncryptedValueNotFound) {
return nil, err
}
// The entry being creted already exists
// The entry being created already exists
if v != nil {
return nil, encryption.ErrEncryptedValueAlreadyExists
}
@@ -311,20 +640,25 @@ func (m *model) create(namespace, name string, version int64, encryptedData []by
name: name,
version: version,
encryptedData: encryptedData,
dataKeyId: dataKeyId,
})
return &contracts.EncryptedValue{
Namespace: namespace,
Name: name,
Version: version,
EncryptedData: encryptedData,
Created: 1,
Updated: 1,
Namespace: namespace,
Name: name,
Version: version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: dataKeyId,
EncryptedData: encryptedData,
},
Created: 1,
Updated: 1,
}, nil
}
func (m *model) update(namespace, name string, version int64, encryptedData []byte) error {
func (m *model) update(namespace, name string, version int64, encryptedData []byte, dataKeyId string) error {
for _, v := range m.entries {
if v.namespace == namespace && v.name == name && v.version == version {
v.encryptedData = encryptedData
v.dataKeyId = dataKeyId
return nil
}
}
@@ -336,12 +670,15 @@ func (m *model) get(namespace, name string, version int64) (*contracts.Encrypted
for _, v := range m.entries {
if v.namespace == namespace && v.name == name && v.version == version {
return &contracts.EncryptedValue{
Namespace: namespace,
Name: name,
Version: version,
EncryptedData: v.encryptedData,
Created: 1,
Updated: 1,
Namespace: namespace,
Name: name,
Version: version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: v.dataKeyId,
EncryptedData: v.encryptedData,
},
Created: 1,
Updated: 1,
}, nil
}
}
@@ -354,3 +691,25 @@ func (m *model) delete(namespace, name string, version int64) error {
})
return nil
}
// mockGlobalEncryptedValueStorage is a mock implementation of contracts.GlobalEncryptedValueStorage
// used for testing error conditions in the migration executor
type mockGlobalEncryptedValueStorage struct {
encryptedValues []*contracts.EncryptedValue
listAllError error
countAllError error
}
func (m *mockGlobalEncryptedValueStorage) ListAll(ctx context.Context, opts contracts.ListOpts, untilTime *int64) ([]*contracts.EncryptedValue, error) {
if m.listAllError != nil {
return nil, m.listAllError
}
return m.encryptedValues, nil
}
func (m *mockGlobalEncryptedValueStorage) CountAll(ctx context.Context, untilTime *int64) (int64, error) {
if m.countAllError != nil {
return 0, m.countAllError
}
return int64(len(m.encryptedValues)), nil
}
+1
View File
@@ -74,6 +74,7 @@ type updateEncryptedValue struct {
Name string
Version int64
EncryptedData []byte
DataKeyID string
Updated int64
}
@@ -24,6 +24,7 @@ func TestEncryptedValueQueries(t *testing.T) {
Name: "n1",
Version: 1,
EncryptedData: []byte("secret"),
DataKeyID: "test-data-key-id",
Created: 1234,
Updated: 5678,
},
@@ -50,6 +51,7 @@ func TestEncryptedValueQueries(t *testing.T) {
Name: "n1",
Version: 1,
EncryptedData: []byte("secret"),
DataKeyID: "test-data-key-id",
Updated: 5679,
},
},
@@ -3,6 +3,7 @@ INSERT INTO `secret_encrypted_value` (
`name`,
`version`,
`encrypted_data`,
`data_key_id`,
`created`,
`updated`
) VALUES (
@@ -10,6 +11,7 @@ INSERT INTO `secret_encrypted_value` (
'n1',
1,
'[115 101 99 114 101 116]',
'test-data-key-id',
1234,
5678
);
@@ -3,6 +3,7 @@ SELECT
`name`,
`version`,
`encrypted_data`,
`data_key_id`,
`created`,
`updated`
FROM
@@ -3,6 +3,7 @@ SELECT
`name`,
`version`,
`encrypted_data`,
`data_key_id`,
`created`,
`updated`
FROM
@@ -3,6 +3,7 @@ SELECT
`name`,
`version`,
`encrypted_data`,
`data_key_id`,
`created`,
`updated`
FROM
@@ -3,6 +3,7 @@ SELECT
`name`,
`version`,
`encrypted_data`,
`data_key_id`,
`created`,
`updated`
FROM
@@ -3,6 +3,7 @@ SELECT
`name`,
`version`,
`encrypted_data`,
`data_key_id`,
`created`,
`updated`
FROM
@@ -2,6 +2,7 @@ UPDATE
`secret_encrypted_value`
SET
`encrypted_data` = '[115 101 99 114 101 116]',
`data_key_id` = 'test-data-key-id',
`updated` = 5679
WHERE
`namespace` = 'ns' AND
@@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" (
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
) VALUES (
@@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" (
'n1',
1,
'[115 101 99 114 101 116]',
'test-data-key-id',
1234,
5678
);
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -2,6 +2,7 @@ UPDATE
"secret_encrypted_value"
SET
"encrypted_data" = '[115 101 99 114 101 116]',
"data_key_id" = 'test-data-key-id',
"updated" = 5679
WHERE
"namespace" = 'ns' AND
@@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" (
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
) VALUES (
@@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" (
'n1',
1,
'[115 101 99 114 101 116]',
'test-data-key-id',
1234,
5678
);
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -3,6 +3,7 @@ SELECT
"name",
"version",
"encrypted_data",
"data_key_id",
"created",
"updated"
FROM
@@ -2,6 +2,7 @@ UPDATE
"secret_encrypted_value"
SET
"encrypted_data" = '[115 101 99 114 101 116]',
"data_key_id" = 'test-data-key-id',
"updated" = 5679
WHERE
"namespace" = 'ns' AND