diff --git a/Makefile b/Makefile index 852e5b2c0..af3982160 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,7 @@ protobuild: @protoc-go-inject-tag -input=./internal/auth/password/store/password.pb.go @protoc-go-inject-tag -input=./internal/auth/password/store/argon2.pb.go @protoc-go-inject-tag -input=./internal/kms/store/root_key.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/database_key.pb.go @protoc-go-inject-tag -input=./internal/target/store/target.pb.go @rm -R ${TMP_DIR} diff --git a/internal/db/migrations/postgres/31_keys.up.sql b/internal/db/migrations/postgres/31_keys.up.sql index 5fe819aa1..0fd234c24 100644 --- a/internal/db/migrations/postgres/31_keys.up.sql +++ b/internal/db/migrations/postgres/31_keys.up.sql @@ -91,7 +91,7 @@ before insert on kms_root_key_version create table kms_database_key ( private_id wt_private_id primary key, - root_key_id wt_private_id not null + root_key_id wt_private_id not null unique -- there can be only one database dek per root key references kms_root_key(private_id) on delete cascade on update cascade, @@ -147,7 +147,7 @@ before insert on kms_database_key_version create table kms_oplog_key ( private_id wt_private_id primary key, - root_key_id wt_private_id not null + root_key_id wt_private_id not null unique -- there can be only one oplog dek per root key references kms_root_key(private_id) on delete cascade on update cascade, @@ -203,7 +203,7 @@ before insert on kms_oplog_key_version create table kms_session_key ( private_id wt_private_id primary key, - root_key_id wt_private_id not null + root_key_id wt_private_id not null unique -- there can be only one session dek per root key references kms_root_key(private_id) on delete cascade on update cascade, diff --git a/internal/kms/database_key.go b/internal/kms/database_key.go new file mode 100644 index 000000000..b042c2327 --- /dev/null +++ b/internal/kms/database_key.go @@ -0,0 +1,80 @@ +package kms + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultDatabaseKeyTableName = "kms_database_key" +) + +type DatabaseKey struct { + *store.DatabaseKey + tableName string `gorm:"-"` +} + +// NewDatabaseKey creates a new in memory database key. No options +// are currently supported. +func NewDatabaseKey(rootKeyId string, opt ...Option) (*DatabaseKey, error) { + if rootKeyId == "" { + return nil, fmt.Errorf("new root key: missing root key id: %w", db.ErrInvalidParameter) + } + c := &DatabaseKey{ + DatabaseKey: &store.DatabaseKey{ + RootKeyId: rootKeyId, + }, + } + return c, nil +} + +// AllocDatabaseKey will allocate a DatabaseKey +func AllocDatabaseKey() DatabaseKey { + return DatabaseKey{ + DatabaseKey: &store.DatabaseKey{}, + } +} + +// Clone creates a clone of the DatabaseKey +func (k *DatabaseKey) Clone() interface{} { + cp := proto.Clone(k.DatabaseKey) + return &DatabaseKey{ + DatabaseKey: cp.(*store.DatabaseKey), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the kms root +// before it's written. +func (k *DatabaseKey) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("database key vet for write: missing private id: %w", db.ErrInvalidParameter) + } + switch opType { + case db.CreateOp: + if k.RootKeyId == "" { + return fmt.Errorf("database key vet for write: missing root key id: %w", db.ErrInvalidParameter) + } + case db.UpdateOp: + return fmt.Errorf("database key vet for write: key is immutable: %w", db.ErrInvalidParameter) + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *DatabaseKey) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultDatabaseKeyTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (k *DatabaseKey) SetTableName(n string) { + k.tableName = n +} diff --git a/internal/kms/database_key_test.go b/internal/kms/database_key_test.go new file mode 100644 index 000000000..c6e7d28db --- /dev/null +++ b/internal/kms/database_key_test.go @@ -0,0 +1,206 @@ +package kms_test + +import ( + "context" + "errors" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// NOTE: there are no update tests since all the DatabaseKey attributes are +// immutable and those tests are covered by TestDatabaseKey_ImmutableFields + +func TestDatabaseKey_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + + type args struct { + rootKeyId string + opt []kms.Option + } + tests := []struct { + name string + args args + want *kms.DatabaseKey + wantErr bool + wantIsErr error + create bool + wantCreateErr bool + }{ + { + name: "empty-rootKeyId", + args: args{}, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "valid", + args: args{ + rootKeyId: rk.PrivateId, + }, + want: func() *kms.DatabaseKey { + k := kms.AllocDatabaseKey() + k.RootKeyId = rk.PrivateId + return &k + }(), + create: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(kms.AllocDatabaseKey()).Error) + got, err := kms.NewDatabaseKey(tt.args.rootKeyId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Is(err, tt.wantIsErr)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + id, err := db.NewPublicId(kms.DatabaseKeyPrefix) + require.NoError(err) + got.PrivateId = id + err = db.New(conn).Create(context.Background(), got) + if tt.wantCreateErr { + assert.Error(err) + return + } else { + assert.NoError(err) + } + } + }) + } +} + +func TestDatabaseKey_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocDatabaseKey()).Error) + + tests := []struct { + name string + key *kms.DatabaseKey + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestDatabaseKey(t, conn, rk.PrivateId), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.DatabaseKey { + k := kms.AllocDatabaseKey() + id, err := db.NewPublicId(kms.DatabaseKeyPrefix) + require.NoError(t, err) + k.PrivateId = id + k.RootKeyId = rk.PrivateId + return &k + }(), + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + deleteKey := kms.AllocDatabaseKey() + deleteKey.PrivateId = tt.key.PrivateId + deletedRows, err := rw.Delete(context.Background(), &deleteKey) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundKey := kms.AllocDatabaseKey() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestDatabaseKey_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + k := kms.TestDatabaseKey(t, conn, rk.PrivateId) + cp := k.Clone() + assert.True(proto.Equal(cp.(*kms.DatabaseKey).DatabaseKey, k.DatabaseKey)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + org2, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + rk2 := kms.TestRootKey(t, conn, org2.PublicId) + k := kms.TestDatabaseKey(t, conn, rk.PrivateId) + k2 := kms.TestDatabaseKey(t, conn, rk2.PrivateId) + + cp := k.Clone() + assert.True(!proto.Equal(cp.(*kms.DatabaseKey).DatabaseKey, k2.DatabaseKey)) + }) +} + +func TestDatabaseKey_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultDatabaseKeyTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := kms.AllocDatabaseKey() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocDatabaseKey() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/kms/database_key_version.go b/internal/kms/database_key_version.go new file mode 100644 index 000000000..e0e6208cd --- /dev/null +++ b/internal/kms/database_key_version.go @@ -0,0 +1,117 @@ +package kms + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms/store" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/structwrapping" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultDatabaseKeyVersionTableName = "kms_database_key_version" +) + +type DatabaseKeyVersion struct { + *store.DatabaseKeyVersion + tableName string `gorm:"-"` +} + +// NewDatabaseKeyVersion creates a new in memory database key version. No options are +// currently supported. +func NewDatabaseKeyVersion(databaseKeyId string, key []byte, rootKeyVersionId string, opt ...Option) (*DatabaseKeyVersion, error) { + if databaseKeyId == "" { + return nil, fmt.Errorf("new database key version: missing database key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("new database key version: missing key: %w", db.ErrInvalidParameter) + } + if rootKeyVersionId == "" { + return nil, fmt.Errorf("new database key version: missing root key version id: %w", db.ErrInvalidParameter) + } + + k := &DatabaseKeyVersion{ + DatabaseKeyVersion: &store.DatabaseKeyVersion{ + DatabaseKeyId: databaseKeyId, + RootKeyVersionId: rootKeyVersionId, + Key: key, + }, + } + return k, nil +} + +// AllocDatabaseKeyVersion allocates a DatabaseKeyVersion +func AllocDatabaseKeyVersion() DatabaseKeyVersion { + return DatabaseKeyVersion{ + DatabaseKeyVersion: &store.DatabaseKeyVersion{}, + } +} + +// Clone creates a clone of the DatabaseKeyVersion +func (k *DatabaseKeyVersion) Clone() interface{} { + cp := proto.Clone(k.DatabaseKeyVersion) + return &DatabaseKeyVersion{ + DatabaseKeyVersion: cp.(*store.DatabaseKeyVersion), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the database key +// version before it's written. +func (k *DatabaseKeyVersion) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("database key version vet for write: missing private id: %w", db.ErrInvalidParameter) + } + switch opType { + case db.CreateOp: + if k.CtKey == nil { + return fmt.Errorf("database key version vet for write: missing key: %w", db.ErrInvalidParameter) + } + if k.DatabaseKeyId == "" { + return fmt.Errorf("database key version vet for write: missing database key id: %w", db.ErrInvalidParameter) + } + if k.RootKeyVersionId == "" { + return fmt.Errorf("database key version vet for write: missing root key version id: %w", db.ErrInvalidParameter) + } + case db.UpdateOp: + return fmt.Errorf("database key version vet for write: key is immutable: %w", db.ErrInvalidParameter) + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *DatabaseKeyVersion) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultDatabaseKeyVersionTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (k *DatabaseKeyVersion) SetTableName(n string) { + k.tableName = n +} + +// Encrypt will encrypt the database key version's key +func (k *DatabaseKeyVersion) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.DatabaseKeyVersion directly + if err := structwrapping.WrapStruct(ctx, cipher, k.DatabaseKeyVersion, nil); err != nil { + return fmt.Errorf("error encrypting kms database key version: %w", err) + } + return nil +} + +// Decrypt will decrypt the database key version's key +func (k *DatabaseKeyVersion) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.DatabaseKeyVersion directly + if err := structwrapping.UnwrapStruct(ctx, cipher, k.DatabaseKeyVersion, nil); err != nil { + return fmt.Errorf("error decrypting kms database key version: %w", err) + } + return nil +} diff --git a/internal/kms/database_key_version_test.go b/internal/kms/database_key_version_test.go new file mode 100644 index 000000000..3e1344e46 --- /dev/null +++ b/internal/kms/database_key_version_test.go @@ -0,0 +1,249 @@ +package kms_test + +import ( + "context" + "errors" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// NOTE: there are no update tests since all the DatabaseKeyVersion attributes are +// immutable and those tests are covered by TestDatabaseKey_ImmutableFields + +func TestDatabaseKeyVersion_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + rkv, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + + type args struct { + databaseKeyId string + key []byte + rootKeyVersionId string + opt []kms.Option + } + tests := []struct { + name string + args args + want *kms.DatabaseKeyVersion + wantErr bool + wantIsErr error + create bool + wantCreateErr bool + }{ + { + name: "empty-database-id", + args: args{ + key: []byte("database key"), + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-key", + args: args{ + databaseKeyId: dk.PrivateId, + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-root-key-version-id", + args: args{ + databaseKeyId: dk.PrivateId, + key: []byte("database key"), + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "valid", + args: args{ + databaseKeyId: dk.PrivateId, + key: []byte("database key"), + rootKeyVersionId: rkv.PrivateId, + }, + want: func() *kms.DatabaseKeyVersion { + k := kms.AllocDatabaseKeyVersion() + k.RootKeyVersionId = rkv.PrivateId + k.Key = []byte("database key") + k.DatabaseKeyId = dk.PrivateId + return &k + }(), + create: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(kms.AllocDatabaseKeyVersion()).Error) + got, err := kms.NewDatabaseKeyVersion(tt.args.databaseKeyId, tt.args.key, tt.args.rootKeyVersionId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Is(err, tt.wantIsErr)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + id, err := db.NewPublicId(kms.DatabaseKeyPrefix) + require.NoError(err) + got.PrivateId = id + err = got.Encrypt(context.Background(), rkvWrapper) + require.NoError(err) + err = db.New(conn).Create(context.Background(), got) + if tt.wantCreateErr { + assert.Error(err) + return + } else { + assert.NoError(err) + } + } + }) + } +} + +func TestDatabaseKeyVersion_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + + require.NoError(t, conn.Where("1=1").Delete(kms.AllocDatabaseKeyVersion()).Error) + + tests := []struct { + name string + key *kms.DatabaseKeyVersion + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestDatabaseKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("database key")), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.DatabaseKeyVersion { + k := kms.AllocDatabaseKeyVersion() + id, err := db.NewPublicId(kms.DatabaseKeyVersionPrefix) + require.NoError(t, err) + k.PrivateId = id + return &k + }(), + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + deleteKey := kms.AllocDatabaseKeyVersion() + deleteKey.PrivateId = tt.key.PrivateId + deletedRows, err := rw.Delete(context.Background(), &deleteKey) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundKey := kms.AllocDatabaseKeyVersion() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestDatabaseKeyVersion_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + + k := kms.TestDatabaseKey(t, conn, rk.PrivateId) + kv := kms.TestDatabaseKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test db key")) + cp := kv.Clone() + assert.True(proto.Equal(cp.(*kms.DatabaseKeyVersion).DatabaseKeyVersion, kv.DatabaseKeyVersion)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + org2, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + rk2 := kms.TestRootKey(t, conn, org2.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + _, rkvWrapper2 := kms.TestRootKeyVersion(t, conn, wrapper, rk2.PrivateId) + + k := kms.TestDatabaseKey(t, conn, rk.PrivateId) + k2 := kms.TestDatabaseKey(t, conn, rk2.PrivateId) + kv := kms.TestDatabaseKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test db key")) + kv2 := kms.TestDatabaseKeyVersion(t, conn, rkvWrapper2, k2.PrivateId, []byte("test db key 2")) + + cp := kv.Clone() + assert.True(!proto.Equal(cp.(*kms.DatabaseKeyVersion).DatabaseKeyVersion, kv2.DatabaseKeyVersion)) + }) +} + +func TestDatabaseKeyVersion_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultDatabaseKeyVersionTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := kms.AllocDatabaseKeyVersion() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocDatabaseKeyVersion() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/kms/ids.go b/internal/kms/ids.go index fbdc8715e..38c2e5fcc 100644 --- a/internal/kms/ids.go +++ b/internal/kms/ids.go @@ -7,8 +7,10 @@ import ( ) const ( - RootKeyPrefix = "krk" - RootKeyVersionPrefix = "krkv" + RootKeyPrefix = "krk" + RootKeyVersionPrefix = "krkv" + DatabaseKeyPrefix = "kdk" + DatabaseKeyVersionPrefix = "kdkv" ) func newRootKeyId() (string, error) { @@ -26,3 +28,19 @@ func newRootKeyVersionId() (string, error) { } return id, nil } + +func newDatabaseKeyId() (string, error) { + id, err := db.NewPublicId(DatabaseKeyPrefix) + if err != nil { + return "", fmt.Errorf("new database key id: %w", err) + } + return id, nil +} + +func newDatabaseKeyVersionId() (string, error) { + id, err := db.NewPublicId(DatabaseKeyVersionPrefix) + if err != nil { + return "", fmt.Errorf("new database key version id: %w", err) + } + return id, nil +} diff --git a/internal/kms/ids_test.go b/internal/kms/ids_test.go index d6149ccbe..5a2fc0288 100644 --- a/internal/kms/ids_test.go +++ b/internal/kms/ids_test.go @@ -20,4 +20,14 @@ func Test_Ids(t *testing.T) { require.NoError(t, err) assert.True(t, strings.HasPrefix(id, RootKeyVersionPrefix+"_")) }) + t.Run("kdk", func(t *testing.T) { + id, err := newDatabaseKeyId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, DatabaseKeyPrefix+"_")) + }) + t.Run("kdkv", func(t *testing.T) { + id, err := newDatabaseKeyVersionId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, DatabaseKeyVersionPrefix+"_")) + }) } diff --git a/internal/kms/immutable_fields_test.go b/internal/kms/immutable_fields_test.go index aefc8d29b..1d965e3d6 100644 --- a/internal/kms/immutable_fields_test.go +++ b/internal/kms/immutable_fields_test.go @@ -16,8 +16,6 @@ import ( // TODO (jimlambrt 8/2020) define tests for immutable fields in the remaining // kms tables: -// kms_database_key -// kms_database_key_version // kms_oplog_key // kms_oplog_key_version // kms_session_key @@ -35,7 +33,7 @@ func TestRootKeyVersion_ImmutableFields(t *testing.T) { require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKeyVersion()).Error) rk := kms.TestRootKey(t, conn, org.PublicId) - new := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key")) + new, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) var tests = []struct { name string @@ -178,3 +176,171 @@ func TestRootKey_ImmutableFields(t *testing.T) { }) } } + +func TestDatabaseKey_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocDatabaseKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + new := kms.TestDatabaseKey(t, conn, rk.PrivateId) + + var tests = []struct { + name string + update *kms.DatabaseKey + fieldMask []string + }{ + { + name: "private_id", + update: func() *kms.DatabaseKey { + k := new.Clone().(*kms.DatabaseKey) + k.PrivateId = "o_thisIsNotAValidId" + return k + }(), + fieldMask: []string{"PrivateId"}, + }, + { + name: "create time", + update: func() *kms.DatabaseKey { + k := new.Clone().(*kms.DatabaseKey) + k.CreateTime = &ts + return k + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "root_key_id", + update: func() *kms.DatabaseKey { + k := new.Clone().(*kms.DatabaseKey) + k.RootKeyId = "o_thisIsNotAValidId" + return k + }(), + fieldMask: []string{"ScopeId"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := new.Clone() + err := rw.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + err = rw.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig.(*kms.DatabaseKey), after.(*kms.DatabaseKey))) + + }) + } +} + +func TestDatabaseKeyVersion_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + rw := db.New(conn) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + new := kms.TestDatabaseKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("database key")) + + var tests = []struct { + name string + update *kms.DatabaseKeyVersion + fieldMask []string + }{ + { + name: "private_id", + update: func() *kms.DatabaseKeyVersion { + k := new.Clone().(*kms.DatabaseKeyVersion) + k.PrivateId = "o_thisIsNotAValidId" + return k + }(), + fieldMask: []string{"PrivateId"}, + }, + { + name: "create time", + update: func() *kms.DatabaseKeyVersion { + k := new.Clone().(*kms.DatabaseKeyVersion) + k.CreateTime = &ts + return k + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "database_key_id", + update: func() *kms.DatabaseKeyVersion { + k := new.Clone().(*kms.DatabaseKeyVersion) + k.DatabaseKeyId = "o_thisIsNotAValidId" + return k + }(), + fieldMask: []string{"RootKeyId"}, + }, + { + name: "root_key_version_id", + update: func() *kms.DatabaseKeyVersion { + k := new.Clone().(*kms.DatabaseKeyVersion) + k.RootKeyVersionId = "o_thisIsNotAValidId" + return k + }(), + fieldMask: []string{"RootKeyId"}, + }, + { + name: "version", + update: func() *kms.DatabaseKeyVersion { + k := new.Clone().(*kms.DatabaseKeyVersion) + k.Version = uint32(22) + return k + }(), + fieldMask: []string{"Version"}, + }, + { + name: "key", + update: func() *kms.DatabaseKeyVersion { + k := new.Clone().(*kms.DatabaseKeyVersion) + k.Key = []byte("updated key") + return k + }(), + fieldMask: []string{"CtKey"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := new.Clone() + err := rw.LookupById(context.Background(), orig) + require.NoError(err) + + err = tt.update.Encrypt(context.Background(), wrapper) + require.NoError(err) + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + err = rw.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig.(*kms.DatabaseKeyVersion), after.(*kms.DatabaseKeyVersion))) + + }) + } +} diff --git a/internal/kms/repository_database_key.go b/internal/kms/repository_database_key.go new file mode 100644 index 000000000..0be1f7672 --- /dev/null +++ b/internal/kms/repository_database_key.go @@ -0,0 +1,147 @@ +package kms + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + wrapping "github.com/hashicorp/go-kms-wrapping" +) + +// CreateDatabaseKey inserts into the repository and returns the new database key and +// database key version. There are no valid options at this time. +func (r *Repository) CreateDatabaseKey(ctx context.Context, rkvWrapper wrapping.Wrapper, key []byte, opt ...Option) (*DatabaseKey, *DatabaseKeyVersion, error) { + var returnedDk, returnedDv interface{} + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + var err error + if returnedDk, returnedDv, err = CreateDatabaseKeyTx(ctx, reader, w, rkvWrapper, key); err != nil { + return err + } + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("create database key: %w", err) + } + return returnedDk.(*DatabaseKey), returnedDv.(*DatabaseKeyVersion), err +} + +// CreateDatabaseKeyTx inserts into the db (via db.Writer) and returns the new database key +// and database key version. This function encapsulates all the work required within +// a db.TxHandler and allows this capability to be shared with the iam repo. +func CreateDatabaseKeyTx(ctx context.Context, r db.Reader, w db.Writer, rkvWrapper wrapping.Wrapper, key []byte) (*DatabaseKey, *DatabaseKeyVersion, error) { + if rkvWrapper == nil { + return nil, nil, fmt.Errorf("create database key: missing key wrapper: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, nil, fmt.Errorf("create database key: missing key: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, nil, fmt.Errorf("create database key: root key version id %s doesn't start with prefix %s: %w", rootKeyVersionId, RootKeyVersionPrefix, db.ErrInvalidParameter) + case rootKeyVersionId == "": + return nil, nil, fmt.Errorf("create database key: missing root key version id: %w", db.ErrInvalidParameter) + } + rv := AllocRootKeyVersion() + rv.PrivateId = rootKeyVersionId + err := r.LookupById(ctx, &rv) + if err != nil { + return nil, nil, fmt.Errorf("create database key: unable to lookup root key version %s: %w", rootKeyVersionId, err) + } + + dk := AllocDatabaseKey() + dv := AllocDatabaseKeyVersion() + id, err := newDatabaseKeyId() + if err != nil { + return nil, nil, fmt.Errorf("create database key: %w", err) + } + dk.PrivateId = id + dk.RootKeyId = rv.RootKeyId + + id, err = newDatabaseKeyVersionId() + if err != nil { + return nil, nil, fmt.Errorf("create database key: %w", err) + } + dv.PrivateId = id + dv.DatabaseKeyId = dk.PrivateId + dv.RootKeyVersionId = rootKeyVersionId + dv.Key = key + if err := dv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, nil, fmt.Errorf("create database key: %w", err) + } + + // no oplog entries for keys + if err := w.Create(ctx, &dk); err != nil { + return nil, nil, fmt.Errorf("create database key: database create: %w", err) + } + // no oplog entries for key versions + if err := w.Create(ctx, &dv); err != nil { + return nil, nil, fmt.Errorf("create database key: version create: %w", err) + } + + return &dk, &dv, err +} + +// LookupDatabaseKey will look up a key in the repository. If the key is not +// found, it will return nil, nil. +func (r *Repository) LookupDatabaseKey(ctx context.Context, privateId string, opt ...Option) (*DatabaseKey, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup database key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocDatabaseKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup database key: failed %w for %s", err, privateId) + } + return &k, nil +} + +// DeleteDatabaseKey deletes the key for the provided id from the +// repository returning a count of the number of records deleted. All options +// are ignored. +func (r *Repository) DeleteDatabaseKey(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete database key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocDatabaseKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete database key: failed %w for %s", err, privateId) + } + + var rowsDeleted int + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + dk := k.Clone() + // no oplog entries for root keys + rowsDeleted, err = w.Delete(ctx, dk) + if err == nil && rowsDeleted > 1 { + return db.ErrMultipleRecords + } + return err + }, + ) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete database key: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// ListDatabaseKeys will list the keys. Supports the WithLimit option. +func (r *Repository) ListDatabaseKeys(ctx context.Context, opt ...Option) ([]*DatabaseKey, error) { + var keys []*DatabaseKey + err := r.list(ctx, &keys, "1=1", nil, opt...) + if err != nil { + return nil, fmt.Errorf("list database keys: %w", err) + } + return keys, nil +} diff --git a/internal/kms/repository_database_key_test.go b/internal/kms/repository_database_key_test.go new file mode 100644 index 000000000..50a957b28 --- /dev/null +++ b/internal/kms/repository_database_key_test.go @@ -0,0 +1,301 @@ +package kms_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/wrappers/aead" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestRepository_CreateDatabaseKey(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := kms.NewRepository(rw, rw) + require.NoError(t, err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + + type args struct { + scopeId string + key []byte + keyWrapper wrapping.Wrapper + opt []kms.Option + } + tests := []struct { + name string + args args + wantErr bool + wantIsError error + }{ + { + name: "valid-org", + args: args{ + scopeId: org.PublicId, + key: []byte("test key"), + keyWrapper: rkvWrapper, + }, + wantErr: false, + }, + { + name: "nil-key", + args: args{ + scopeId: org.PublicId, + keyWrapper: rkvWrapper, + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "empty-key", + args: args{ + scopeId: org.PublicId, + keyWrapper: rkvWrapper, + key: []byte(""), + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "nil-wrapper", + args: args{ + scopeId: org.PublicId, + key: []byte("test key"), + keyWrapper: nil, + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-rkv-wrapper", + args: args{ + scopeId: org.PublicId, + key: []byte("test key"), + keyWrapper: wrapper, + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "wrapper-missing-id", + args: args{ + scopeId: org.PublicId, + key: []byte("test key"), + keyWrapper: func() wrapping.Wrapper { + w := db.TestWrapper(t) + _, err = w.(*aead.Wrapper).SetConfig(map[string]string{ + "key_id": "", + }) + require.NoError(t, err) + return w + }(), + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + dk, dv, err := repo.CreateDatabaseKey(context.Background(), tt.args.keyWrapper, tt.args.key, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Nil(dk) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + return + } + require.NoError(err) + assert.NotNil(dk.CreateTime) + foundKey, err := repo.LookupDatabaseKey(context.Background(), dk.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKey, dk)) + + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, dk.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(err, db.ErrRecordNotFound)) + + assert.NotNil(dv.CreateTime) + foundKeyVersion, err := repo.LookupDatabaseKeyVersion(context.Background(), tt.args.keyWrapper, dv.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKeyVersion, dv)) + + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, dv.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(err, db.ErrRecordNotFound)) + }) + } +} + +func TestRepository_DeleteDatabaseKey(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := kms.NewRepository(rw, rw) + require.NoError(t, err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + + type args struct { + key *kms.DatabaseKey + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestDatabaseKey(t, conn, rk.PrivateId), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.DatabaseKey { + k := kms.AllocDatabaseKey() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.DatabaseKey { + id, err := db.NewPublicId(kms.RootKeyPrefix) + require.NoError(t, err) + k := kms.AllocDatabaseKey() + k.PrivateId = id + require.NoError(t, err) + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrRecordNotFound, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + deletedRows, err := repo.DeleteDatabaseKey(context.Background(), tt.args.key.PrivateId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.Equal(0, deletedRows) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, tt.args.key.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + return + } + require.NoError(err) + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundKey, err := repo.LookupDatabaseKey(context.Background(), tt.args.key.PrivateId) + assert.Error(err) + assert.Nil(foundKey) + assert.True(errors.Is(err, db.ErrRecordNotFound)) + + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, tt.args.key.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestRepository_ListDatabaseKeys(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + const testLimit = 10 + rw := db.New(conn) + repo, err := kms.NewRepository(rw, rw, kms.WithLimit(testLimit)) + require.NoError(t, err) + wrapper := db.TestWrapper(t) + + type args struct { + opt []kms.Option + } + tests := []struct { + name string + createCnt int + args args + wantCnt int + wantErr bool + }{ + { + name: "no-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + opt: []kms.Option{kms.WithLimit(-1)}, + }, + wantCnt: repo.DefaultLimit() + 1, // org and project both have keys, plus global scope + wantErr: false, + }, + { + name: "default-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{}, + wantCnt: repo.DefaultLimit(), + wantErr: false, + }, + { + name: "custom-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + opt: []kms.Option{kms.WithLimit(3)}, + }, + wantCnt: 3, + wantErr: false, + }, + } + for _, tt := range tests { + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + for i := 0; i < tt.createCnt; i++ { + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(conn.Where("scope_id=?", []interface{}{proj.PublicId}).Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, proj.PublicId) + kms.TestDatabaseKey(t, conn, rk.PrivateId) + require.NoError(err) + } + got, err := repo.ListDatabaseKeys(context.Background(), tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got)) + }) + } +} diff --git a/internal/kms/repository_database_key_version.go b/internal/kms/repository_database_key_version.go new file mode 100644 index 000000000..7ae842547 --- /dev/null +++ b/internal/kms/repository_database_key_version.go @@ -0,0 +1,160 @@ +package kms + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + wrapping "github.com/hashicorp/go-kms-wrapping" +) + +// CreateDatabaseKeyVersion inserts into the repository and returns the new key +// version with its PrivateId. There are no valid options at this time. +func (r *Repository) CreateDatabaseKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, databaseKeyId string, key []byte, opt ...Option) (*DatabaseKeyVersion, error) { + if rkvWrapper == nil { + return nil, fmt.Errorf("create database key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, fmt.Errorf("create database key version: root key version id %s doesn't start with prefix %s: %w", rootKeyVersionId, RootKeyVersionPrefix, db.ErrInvalidParameter) + case rootKeyVersionId == "": + return nil, fmt.Errorf("create database key version: missing root key version id: %w", db.ErrInvalidParameter) + } + if databaseKeyId == "" { + return nil, fmt.Errorf("create database key version: missing database key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("create database key version: missing key: %w", db.ErrInvalidParameter) + } + kv := AllocDatabaseKeyVersion() + id, err := newDatabaseKeyVersionId() + if err != nil { + return nil, fmt.Errorf("create database key version: %w", err) + } + kv.PrivateId = id + kv.RootKeyVersionId = rootKeyVersionId + kv.Key = key + kv.DatabaseKeyId = databaseKeyId + if err := kv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("create database key version: encrypt: %w", err) + } + + var returnedKey interface{} + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedKey = kv.Clone() + // no oplog entries for root key version + if err := w.Create(ctx, returnedKey); err != nil { + return err + } + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("create database key version: %w for %s database key id", err, kv.DatabaseKeyId) + } + return returnedKey.(*DatabaseKeyVersion), err +} + +// LookupDatabaseKeyVersion will look up a key version in the repository. If +// the key version is not found, it will return nil, nil. +func (r *Repository) LookupDatabaseKeyVersion(ctx context.Context, keyWrapper wrapping.Wrapper, privateId string, opt ...Option) (*DatabaseKeyVersion, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup database key version: missing private id: %w", db.ErrInvalidParameter) + } + if keyWrapper == nil { + return nil, fmt.Errorf("lookup database key version: missing key wrapper: %w", db.ErrInvalidParameter) + } + k := AllocDatabaseKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup database key version: failed %w for %s", err, privateId) + } + if err := k.Decrypt(ctx, keyWrapper); err != nil { + return nil, fmt.Errorf("lookup database key version: decrypt: %w", err) + } + return &k, nil +} + +// DeleteDatabaseKeyVersion deletes the key version for the provided id from the +// repository returning a count of the number of records deleted. All options +// are ignored. +func (r *Repository) DeleteDatabaseKeyVersion(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete database key version: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocDatabaseKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete database key version: failed %w for %s", err, privateId) + } + + var rowsDeleted int + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + dk := k.Clone() + // no oplog entries for the key version + rowsDeleted, err = w.Delete(ctx, dk) + if err == nil && rowsDeleted > 1 { + return db.ErrMultipleRecords + } + return err + }, + ) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete database key version: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// LatestDatabaseKeyVersion searches for the key version with the highest +// version number. When no results are found, it returns nil, +// db.ErrRecordNotFound. +func (r *Repository) LatestDatabaseKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, databaseKeyId string, opt ...Option) (*DatabaseKeyVersion, error) { + if databaseKeyId == "" { + return nil, fmt.Errorf("latest database key version: missing database key id: %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("latest database key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var foundKeys []*DatabaseKeyVersion + if err := r.reader.SearchWhere(ctx, &foundKeys, "database_key_id = ?", []interface{}{databaseKeyId}, db.WithLimit(1), db.WithOrder("version desc")); err != nil { + return nil, fmt.Errorf("latest database key version: failed %w for %s", err, databaseKeyId) + } + if len(foundKeys) == 0 { + return nil, db.ErrRecordNotFound + } + if err := foundKeys[0].Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("latest database key version: %w", err) + } + return foundKeys[0], nil +} + +// ListDatabaseKeyVersions will lists versions of a key. Supports the WithLimit option. +func (r *Repository) ListDatabaseKeyVersions(ctx context.Context, rkvWrapper wrapping.Wrapper, databaseKeyId string, opt ...Option) ([]*DatabaseKeyVersion, error) { + if databaseKeyId == "" { + return nil, fmt.Errorf("list database key versions: missing database key id %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("list database key versions: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var versions []*DatabaseKeyVersion + err := r.list(ctx, &versions, "database_key_id = ?", []interface{}{databaseKeyId}, opt...) + if err != nil { + return nil, fmt.Errorf("list database key versions: %w", err) + } + for i, k := range versions { + if err := k.Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("list database key versions: error decrypting key num %d: %w", i, err) + } + } + return versions, nil +} diff --git a/internal/kms/repository_database_key_version_test.go b/internal/kms/repository_database_key_version_test.go new file mode 100644 index 000000000..54a3c2456 --- /dev/null +++ b/internal/kms/repository_database_key_version_test.go @@ -0,0 +1,373 @@ +package kms_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestRepository_CreateDatabaseKeyVersion(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := kms.NewRepository(rw, rw) + require.NoError(t, err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + + type args struct { + key []byte + databaseKeyId string + keyWrapper wrapping.Wrapper + opt []kms.Option + } + tests := []struct { + name string + args args + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: []byte("test key"), + keyWrapper: rkvWrapper, + databaseKeyId: dk.PrivateId, + }, + wantErr: false, + }, + { + name: "invalid-rkv-wrapper", + args: args{ + key: []byte("test key"), + keyWrapper: wrapper, + databaseKeyId: dk.PrivateId, + }, + wantErr: true, + }, + { + name: "empty-key", + args: args{ + key: nil, + keyWrapper: wrapper, + databaseKeyId: dk.PrivateId, + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "nil-wrapper", + args: args{ + key: []byte("test key"), + keyWrapper: nil, + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + k, err := repo.CreateDatabaseKeyVersion(context.Background(), tt.args.keyWrapper, tt.args.databaseKeyId, tt.args.key, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Nil(k) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + return + } + require.NoError(err) + assert.NotNil(k.CreateTime) + foundKey, err := repo.LookupDatabaseKeyVersion(context.Background(), tt.args.keyWrapper, k.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKey, k)) + + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, k.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestRepository_DeleteDatabaseKeyVersion(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := kms.NewRepository(rw, rw) + require.NoError(t, err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + + type args struct { + key *kms.DatabaseKeyVersion + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestDatabaseKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("database key")), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.DatabaseKeyVersion { + k := kms.AllocDatabaseKeyVersion() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.DatabaseKeyVersion { + id, err := db.NewPublicId(kms.DatabaseKeyPrefix) + require.NoError(t, err) + k := kms.AllocDatabaseKeyVersion() + k.PrivateId = id + require.NoError(t, err) + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrRecordNotFound, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + deletedRows, err := repo.DeleteDatabaseKeyVersion(context.Background(), tt.args.key.PrivateId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.Equal(0, deletedRows) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + err = db.TestVerifyOplog(t, rw, tt.args.key.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + return + } + require.NoError(err) + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundKey, err := repo.LookupDatabaseKeyVersion(context.Background(), wrapper, tt.args.key.PrivateId) + assert.Error(err) + assert.Nil(foundKey) + assert.True(errors.Is(err, db.ErrRecordNotFound)) + + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, tt.args.key.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestRepository_LatestDatabaseKeyVersion(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := kms.NewRepository(rw, rw) + require.NoError(t, err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + + tests := []struct { + name string + createCnt int + keyWrapper wrapping.Wrapper + wantVersion uint32 + wantErr bool + wantIsError error + }{ + { + name: "5", + createCnt: 5, + keyWrapper: rkvWrapper, + wantVersion: 5, + wantErr: false, + }, + { + name: "1", + createCnt: 1, + keyWrapper: rkvWrapper, + wantVersion: 1, + wantErr: false, + }, + { + name: "0", + createCnt: 0, + keyWrapper: rkvWrapper, + wantErr: true, + wantIsError: db.ErrRecordNotFound, + }, + { + name: "nil-wrapper", + createCnt: 5, + keyWrapper: nil, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(kms.AllocDatabaseKeyVersion()).Error) + testKeys := []*kms.DatabaseKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestDatabaseKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("test database key")) + testKeys = append(testKeys, k) + } + assert.Equal(tt.createCnt, len(testKeys)) + got, err := repo.LatestDatabaseKeyVersion(context.Background(), tt.keyWrapper, dk.PrivateId) + if tt.wantErr { + require.Error(err) + assert.Nil(got) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + return + } + require.NoError(err) + require.NotNil(got) + assert.Equal(tt.wantVersion, got.Version) + }) + } +} + +func TestRepository_ListDatabaseKeyVersions(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + const testLimit = 10 + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := kms.NewRepository(rw, rw, kms.WithLimit(testLimit)) + require.NoError(t, err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rkvWrapper := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + + type args struct { + databaseKeyId string + keyWrapper wrapping.Wrapper + opt []kms.Option + } + tests := []struct { + name string + createCnt int + args args + wantCnt int + wantErr bool + }{ + { + name: "no-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + databaseKeyId: dk.PrivateId, + keyWrapper: rkvWrapper, + opt: []kms.Option{kms.WithLimit(-1)}, + }, + wantCnt: repo.DefaultLimit() + 1, + wantErr: false, + }, + { + name: "default-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + keyWrapper: rkvWrapper, + databaseKeyId: dk.PrivateId, + }, + wantCnt: repo.DefaultLimit(), + wantErr: false, + }, + { + name: "custom-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + keyWrapper: rkvWrapper, + databaseKeyId: dk.PrivateId, + opt: []kms.Option{kms.WithLimit(3)}, + }, + wantCnt: 3, + wantErr: false, + }, + { + name: "bad-org", + createCnt: 1, + args: args{ + keyWrapper: rkvWrapper, + databaseKeyId: "bad-id", + }, + wantCnt: 0, + wantErr: false, + }, + { + name: "nil-wrapper", + createCnt: 1, + args: args{ + keyWrapper: nil, + databaseKeyId: dk.PrivateId, + }, + wantCnt: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(kms.AllocDatabaseKeyVersion()).Error) + keyVersions := []*kms.DatabaseKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestDatabaseKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("database key")) + keyVersions = append(keyVersions, k) + } + assert.Equal(tt.createCnt, len(keyVersions)) + got, err := repo.ListDatabaseKeyVersions(context.Background(), tt.args.keyWrapper, tt.args.databaseKeyId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got)) + }) + } +} diff --git a/internal/kms/repository_root_key.go b/internal/kms/repository_root_key.go index f6d9d1de9..1ff91284a 100644 --- a/internal/kms/repository_root_key.go +++ b/internal/kms/repository_root_key.go @@ -32,8 +32,7 @@ func (r *Repository) CreateRootKey(ctx context.Context, keyWrapper wrapping.Wrap // CreateRootKeyTx inserts into the db (via db.Writer) and returns the new root key // and root key version. This function encapsulates all the work required within -// a db.TxHandler and allows this capability to be shared with the iam repo via -// a common pkg without circular dependencies: kms/common.CreateRootKeyTx +// a db.TxHandler and allows this capability to be shared with the iam repo. func CreateRootKeyTx(ctx context.Context, w db.Writer, keyWrapper wrapping.Wrapper, scopeId string, key []byte) (*RootKey, *RootKeyVersion, error) { if scopeId == "" { return nil, nil, fmt.Errorf("create root key: missing scope id: %w", db.ErrInvalidParameter) diff --git a/internal/kms/repository_root_key_version_test.go b/internal/kms/repository_root_key_version_test.go index fe47591b1..4375f6ebf 100644 --- a/internal/kms/repository_root_key_version_test.go +++ b/internal/kms/repository_root_key_version_test.go @@ -129,7 +129,10 @@ func TestRepository_DeleteRootKeyVersion(t *testing.T) { { name: "valid", args: args{ - key: kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key")), + key: func() *kms.RootKeyVersion { + k, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + return k + }(), }, wantRowsDeleted: 1, wantErr: false, @@ -247,7 +250,8 @@ func TestRepository_LatestRootKeyVersion(t *testing.T) { require.NoError(conn.Where("1=1").Delete(kms.AllocRootKeyVersion()).Error) testKeys := []*kms.RootKeyVersion{} for i := 0; i < tt.createCnt; i++ { - testKeys = append(testKeys, kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key"))) + k, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + testKeys = append(testKeys, k) } assert.Equal(tt.createCnt, len(testKeys)) got, err := repo.LatestRootKeyVersion(context.Background(), tt.keyWrapper, rk.PrivateId) @@ -349,7 +353,8 @@ func TestRepository_ListRootKeyVersions(t *testing.T) { require.NoError(conn.Where("1=1").Delete(kms.AllocRootKeyVersion()).Error) testRootKeyVersions := []*kms.RootKeyVersion{} for i := 0; i < tt.createCnt; i++ { - testRootKeyVersions = append(testRootKeyVersions, kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key"))) + k, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + testRootKeyVersions = append(testRootKeyVersions, k) } assert.Equal(tt.createCnt, len(testRootKeyVersions)) got, err := repo.ListRootKeyVersions(context.Background(), tt.args.keyWrapper, tt.args.rootKeyId, tt.args.opt...) diff --git a/internal/kms/root_key.go b/internal/kms/root_key.go index db3f5b7ef..2f53fa66d 100644 --- a/internal/kms/root_key.go +++ b/internal/kms/root_key.go @@ -55,10 +55,13 @@ func (k *RootKey) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType if k.PrivateId == "" { return fmt.Errorf("root key vet for write: missing private id: %w", db.ErrInvalidParameter) } - if opType == db.CreateOp { + switch opType { + case db.CreateOp: if k.ScopeId == "" { return fmt.Errorf("root key vet for write: missing scope id: %w", db.ErrInvalidParameter) } + case db.UpdateOp: + return fmt.Errorf("root key vet for write: key is immutable: %w", db.ErrInvalidParameter) } return nil } diff --git a/internal/kms/root_key_version.go b/internal/kms/root_key_version.go index 1b0595bd8..77817ae45 100644 --- a/internal/kms/root_key_version.go +++ b/internal/kms/root_key_version.go @@ -60,13 +60,16 @@ func (k *RootKeyVersion) VetForWrite(ctx context.Context, r db.Reader, opType db if k.PrivateId == "" { return fmt.Errorf("root key version vet for write: missing private id: %w", db.ErrInvalidParameter) } - if opType == db.CreateOp { + switch opType { + case db.CreateOp: if k.CtKey == nil { return fmt.Errorf("root key version vet for write: missing key: %w", db.ErrInvalidParameter) } if k.RootKeyId == "" { return fmt.Errorf("root key version vet for write: missing root key id: %w", db.ErrInvalidParameter) } + case db.UpdateOp: + return fmt.Errorf("root key version vet for write: key is immutable: %w", db.ErrInvalidParameter) } return nil } diff --git a/internal/kms/root_key_version_test.go b/internal/kms/root_key_version_test.go index 72f44af70..a7887c9e7 100644 --- a/internal/kms/root_key_version_test.go +++ b/internal/kms/root_key_version_test.go @@ -118,7 +118,7 @@ func TestRootKeyVersion_Delete(t *testing.T) { }{ { name: "valid", - key: kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key")), + key: func() *kms.RootKeyVersion { k, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId); return k }(), wantErr: false, wantRowsDeleted: 1, }, @@ -169,7 +169,7 @@ func TestRootKeyVersion_Clone(t *testing.T) { org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) rk := kms.TestRootKey(t, conn, org.PublicId) - k := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key")) + k, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) cp := k.Clone() assert.True(proto.Equal(cp.(*kms.RootKeyVersion).RootKeyVersion, k.RootKeyVersion)) }) @@ -178,8 +178,8 @@ func TestRootKeyVersion_Clone(t *testing.T) { org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) require.NoError(t, conn.Where("1=1").Delete(kms.AllocRootKey()).Error) rk := kms.TestRootKey(t, conn, org.PublicId) - k := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key")) - k2 := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key")) + k, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) + k2, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) cp := k.Clone() assert.True(!proto.Equal(cp.(*kms.RootKeyVersion).RootKeyVersion, k2.RootKeyVersion)) }) diff --git a/internal/kms/store/database_key.pb.go b/internal/kms/store/database_key.pb.go new file mode 100644 index 000000000..137f5f7d4 --- /dev/null +++ b/internal/kms/store/database_key.pb.go @@ -0,0 +1,335 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.12.4 +// source: controller/storage/kms/store/v1/database_key.proto + +package store + +import ( + proto "github.com/golang/protobuf/proto" + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/internal/gen/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type DatabaseKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // private_id is used to access the key via an API + // @inject_tag: gorm:"primary_key" + PrivateId string `protobuf:"bytes,10,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty" gorm:"primary_key"` + // root key id for the key + // @inject_tag: `gorm:"default:null"` + RootKeyId string `protobuf:"bytes,20,opt,name=root_key_id,json=rootKeyId,proto3" json:"root_key_id,omitempty" gorm:"default:null"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *DatabaseKey) Reset() { + *x = DatabaseKey{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_database_key_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DatabaseKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DatabaseKey) ProtoMessage() {} + +func (x *DatabaseKey) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_database_key_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DatabaseKey.ProtoReflect.Descriptor instead. +func (*DatabaseKey) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_database_key_proto_rawDescGZIP(), []int{0} +} + +func (x *DatabaseKey) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *DatabaseKey) GetRootKeyId() string { + if x != nil { + return x.RootKeyId + } + return "" +} + +func (x *DatabaseKey) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +type DatabaseKeyVersion struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // private_id is used to access the key version via an API + // @inject_tag: gorm:"primary_key" + PrivateId string `protobuf:"bytes,10,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty" gorm:"primary_key"` + // database id for the key version + // @inject_tag: `gorm:"default:null"` + DatabaseKeyId string `protobuf:"bytes,20,opt,name=database_key_id,json=databaseKeyId,proto3" json:"database_key_id,omitempty" gorm:"default:null"` + // root_key_version_id of the version of the root key data. + // @inject_tag: `gorm:"default:null"` + RootKeyVersionId string `protobuf:"bytes,30,opt,name=root_key_version_id,json=rootKeyVersionId,proto3" json:"root_key_version_id,omitempty" gorm:"default:null"` + // plain-text of the key data. we are NOT storing this plain-text key + // in the db. + // @inject_tag: `gorm:"-" wrapping:"pt,key_data"` + Key []byte `protobuf:"bytes,40,opt,name=key,proto3" json:"key,omitempty" gorm:"-" wrapping:"pt,key_data"` + // ciphertext key data stored in the database + // @inject_tag: `gorm:"column:key;not_null" wrapping:"ct,key_data"` + CtKey []byte `protobuf:"bytes,50,opt,name=ct_key,json=ctKey,proto3" json:"ct_key,omitempty" gorm:"column:key;not_null" wrapping:"ct,key_data"` + // version of the key data. This is not used for optimistic locking, since + // key versions are immutable. It's just the version of the key. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,60,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,70,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *DatabaseKeyVersion) Reset() { + *x = DatabaseKeyVersion{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_database_key_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DatabaseKeyVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DatabaseKeyVersion) ProtoMessage() {} + +func (x *DatabaseKeyVersion) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_database_key_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DatabaseKeyVersion.ProtoReflect.Descriptor instead. +func (*DatabaseKeyVersion) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_database_key_proto_rawDescGZIP(), []int{1} +} + +func (x *DatabaseKeyVersion) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *DatabaseKeyVersion) GetDatabaseKeyId() string { + if x != nil { + return x.DatabaseKeyId + } + return "" +} + +func (x *DatabaseKeyVersion) GetRootKeyVersionId() string { + if x != nil { + return x.RootKeyVersionId + } + return "" +} + +func (x *DatabaseKeyVersion) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *DatabaseKeyVersion) GetCtKey() []byte { + if x != nil { + return x.CtKey + } + return nil +} + +func (x *DatabaseKeyVersion) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *DatabaseKeyVersion) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +var File_controller_storage_kms_store_v1_database_key_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_database_key_proto_rawDesc = []byte{ + 0x0a, 0x32, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x6b, 0x6d, 0x73, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0x99, 0x01, 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4b, + 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, + 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x6f, 0x6f, 0x74, 0x4b, 0x65, 0x79, 0x49, + 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x9a, + 0x02, 0x0a, 0x12, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, + 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x13, + 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x6f, 0x6f, 0x74, 0x4b, + 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x15, 0x0a, + 0x06, 0x63, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, + 0x74, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x3c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4b, + 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x46, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6b, 0x6d, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_kms_store_v1_database_key_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_database_key_proto_rawDescData = file_controller_storage_kms_store_v1_database_key_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_database_key_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_database_key_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_database_key_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_database_key_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_database_key_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_database_key_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_kms_store_v1_database_key_proto_goTypes = []interface{}{ + (*DatabaseKey)(nil), // 0: controller.storage.kms.store.v1.DatabaseKey + (*DatabaseKeyVersion)(nil), // 1: controller.storage.kms.store.v1.DatabaseKeyVersion + (*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_kms_store_v1_database_key_proto_depIdxs = []int32{ + 2, // 0: controller.storage.kms.store.v1.DatabaseKey.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // 1: controller.storage.kms.store.v1.DatabaseKeyVersion.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_controller_storage_kms_store_v1_database_key_proto_init() } +func file_controller_storage_kms_store_v1_database_key_proto_init() { + if File_controller_storage_kms_store_v1_database_key_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_database_key_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DatabaseKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_kms_store_v1_database_key_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DatabaseKeyVersion); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_kms_store_v1_database_key_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_database_key_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_database_key_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_database_key_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_database_key_proto = out.File + file_controller_storage_kms_store_v1_database_key_proto_rawDesc = nil + file_controller_storage_kms_store_v1_database_key_proto_goTypes = nil + file_controller_storage_kms_store_v1_database_key_proto_depIdxs = nil +} diff --git a/internal/kms/testing.go b/internal/kms/testing.go index 6710b286a..c44cb9823 100644 --- a/internal/kms/testing.go +++ b/internal/kms/testing.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/boundary/internal/db" wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/wrappers/aead" "github.com/jinzhu/gorm" "github.com/stretchr/testify/require" ) @@ -25,10 +26,12 @@ func TestRootKey(t *testing.T, conn *gorm.DB, scopeId string) *RootKey { return k } -func TestRootKeyVersion(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, rootId string, key []byte) *RootKeyVersion { +func TestRootKeyVersion(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, rootId string) (kv *RootKeyVersion, kvWrapper wrapping.Wrapper) { t.Helper() require := require.New(t) rw := db.New(conn) + rootKeyVersionWrapper := db.TestWrapper(t) + key := rootKeyVersionWrapper.(*aead.Wrapper).GetKeyBytes() k, err := NewRootKeyVersion(rootId, key) require.NoError(err) id, err := newRootKeyVersionId() @@ -38,7 +41,11 @@ func TestRootKeyVersion(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, r require.NoError(err) err = rw.Create(context.Background(), k) require.NoError(err) - return k + _, err = rootKeyVersionWrapper.(*aead.Wrapper).SetConfig(map[string]string{ + "key_id": k.GetPrivateId(), + }) + require.NoError(err) + return k, rootKeyVersionWrapper } func TestKms(t *testing.T, conn *gorm.DB, rootWrapper wrapping.Wrapper) *Kms { @@ -53,3 +60,37 @@ func TestKms(t *testing.T, conn *gorm.DB, rootWrapper wrapping.Wrapper) *Kms { require.NoError(err) return kms } + +func TestDatabaseKey(t *testing.T, conn *gorm.DB, rootKeyId string) *DatabaseKey { + t.Helper() + require := require.New(t) + rw := db.New(conn) + require.NoError(conn.Where("root_key_id = ?", rootKeyId).Delete(AllocDatabaseKey()).Error) + k, err := NewDatabaseKey(rootKeyId) + require.NoError(err) + id, err := newDatabaseKeyId() + require.NoError(err) + k.PrivateId = id + k.RootKeyId = rootKeyId + err = rw.Create(context.Background(), k) + require.NoError(err) + return k +} + +func TestDatabaseKeyVersion(t *testing.T, conn *gorm.DB, rootKeyVersionWrapper wrapping.Wrapper, databaseKeyId string, key []byte) *DatabaseKeyVersion { + t.Helper() + require := require.New(t) + rw := db.New(conn) + rootKeyVersionId := rootKeyVersionWrapper.KeyID() + require.NotEmpty(rootKeyVersionId) + k, err := NewDatabaseKeyVersion(databaseKeyId, key, rootKeyVersionId) + require.NoError(err) + id, err := newDatabaseKeyVersionId() + require.NoError(err) + k.PrivateId = id + err = k.Encrypt(context.Background(), rootKeyVersionWrapper) + require.NoError(err) + err = rw.Create(context.Background(), k) + require.NoError(err) + return k +} diff --git a/internal/kms/testing_test.go b/internal/kms/testing_test.go index 9bdc65926..6426fb68d 100644 --- a/internal/kms/testing_test.go +++ b/internal/kms/testing_test.go @@ -30,7 +30,38 @@ func Test_TestRootKeyVersion(t *testing.T) { org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) require.NoError(conn.Where("1=1").Delete(kms.AllocRootKey()).Error) rk := kms.TestRootKey(t, conn, org.PublicId) - k := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId, []byte("test key")) + k, _ := kms.TestRootKeyVersion(t, conn, wrapper, rk.PrivateId) require.NotNil(k) assert.NotEmpty(k.PrivateId) } + +func Test_TestDatabaseKey(t *testing.T) { + t.Helper() + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + k := kms.TestRootKey(t, conn, org.PublicId) + require.NotNil(k) + assert.NotEmpty(k.PrivateId) + + dk := kms.TestDatabaseKey(t, conn, k.PrivateId) + require.NotNil(dk) + assert.NotEmpty(dk.PrivateId) +} + +func Test_TestDatabaseKeyVersion(t *testing.T) { + t.Helper() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + kmsWrapper := db.TestWrapper(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, kmsWrapper)) + require.NoError(conn.Where("1=1").Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, org.PublicId) + _, rootKeyVersionWrapper := kms.TestRootKeyVersion(t, conn, kmsWrapper, rk.PrivateId) + dk := kms.TestDatabaseKey(t, conn, rk.PrivateId) + dv := kms.TestDatabaseKeyVersion(t, conn, rootKeyVersionWrapper, dk.PrivateId, []byte("test dek key")) + require.NotNil(dv) + require.NotEmpty(dv.PrivateId) +} diff --git a/internal/proto/local/controller/storage/kms/store/v1/database_key.proto b/internal/proto/local/controller/storage/kms/store/v1/database_key.proto new file mode 100644 index 000000000..940c03069 --- /dev/null +++ b/internal/proto/local/controller/storage/kms/store/v1/database_key.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +package controller.storage.kms.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/kms/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; + +message DatabaseKey { + // private_id is used to access the key via an API + // @inject_tag: gorm:"primary_key" + string private_id = 10; + + // root key id for the key + // @inject_tag: `gorm:"default:null"` + string root_key_id = 20; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 30; +} + +message DatabaseKeyVersion { + // private_id is used to access the key version via an API + // @inject_tag: gorm:"primary_key" + string private_id = 10; + + // database id for the key version + // @inject_tag: `gorm:"default:null"` + string database_key_id = 20; + + // root_key_version_id of the version of the root key data. + // @inject_tag: `gorm:"default:null"` + string root_key_version_id = 30; + + // plain-text of the key data. we are NOT storing this plain-text key + // in the db. + // @inject_tag: `gorm:"-" wrapping:"pt,key_data"` + bytes key = 40; + + // ciphertext key data stored in the database + // @inject_tag: `gorm:"column:key;not_null" wrapping:"ct,key_data"` + bytes ct_key = 50; + + // version of the key data. This is not used for optimistic locking, since + // key versions are immutable. It's just the version of the key. + // @inject_tag: `gorm:"default:null"` + uint32 version = 60; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 70; +} \ No newline at end of file