From f29869b7154435c31f64d72a030684aa178dd564 Mon Sep 17 00:00:00 2001 From: Jim Date: Wed, 2 Sep 2020 14:37:19 -0400 Subject: [PATCH] support for additional deks: oplog, session and token and new CreateKeysTx() (#321) --- Makefile | 3 + internal/db/migrations/postgres.gen.go | 124 ++++-- .../db/migrations/postgres/31_keys.up.sql | 124 ++++-- internal/iam/repository_scope.go | 11 +- internal/kms/const.go | 53 +++ internal/kms/database_key.go | 2 +- internal/kms/ids.go | 54 +++ internal/kms/ids_test.go | 30 ++ internal/kms/kms.go | 2 +- internal/kms/oplog_key.go | 77 ++++ internal/kms/oplog_key_test.go | 206 ++++++++++ internal/kms/oplog_key_version.go | 114 ++++++ internal/kms/oplog_key_version_test.go | 249 ++++++++++++ internal/kms/repository.go | 112 ++++++ internal/kms/repository_database_key.go | 6 +- internal/kms/repository_database_key_test.go | 4 +- internal/kms/repository_oplog_key.go | 147 +++++++ internal/kms/repository_oplog_key_test.go | 301 ++++++++++++++ internal/kms/repository_oplog_key_version.go | 160 ++++++++ .../kms/repository_oplog_key_version_test.go | 373 ++++++++++++++++++ internal/kms/repository_root_key.go | 6 +- internal/kms/repository_session_key.go | 147 +++++++ internal/kms/repository_session_key_test.go | 301 ++++++++++++++ .../kms/repository_session_key_version.go | 160 ++++++++ .../repository_session_key_version_test.go | 373 ++++++++++++++++++ internal/kms/repository_test.go | 177 +++++++++ internal/kms/repository_token_key.go | 147 +++++++ internal/kms/repository_token_key_test.go | 301 ++++++++++++++ internal/kms/repository_token_key_version.go | 160 ++++++++ .../kms/repository_token_key_version_test.go | 373 ++++++++++++++++++ internal/kms/root_key.go | 2 +- internal/kms/session_key.go | 77 ++++ internal/kms/session_key_test.go | 206 ++++++++++ internal/kms/session_key_version.go | 114 ++++++ internal/kms/session_key_version_test.go | 249 ++++++++++++ internal/kms/store/oplog_key.pb.go | 334 ++++++++++++++++ internal/kms/store/session_key.pb.go | 335 ++++++++++++++++ internal/kms/store/token_key.pb.go | 334 ++++++++++++++++ internal/kms/testing.go | 102 +++++ internal/kms/testing_test.go | 93 +++++ internal/kms/token_key.go | 77 ++++ internal/kms/token_key_test.go | 206 ++++++++++ internal/kms/token_key_version.go | 114 ++++++ internal/kms/token_key_version_test.go | 249 ++++++++++++ .../storage/kms/store/v1/oplog_key.proto | 53 +++ .../storage/kms/store/v1/session_key.proto | 53 +++ .../storage/kms/store/v1/token_key.proto | 53 +++ 47 files changed, 6863 insertions(+), 85 deletions(-) create mode 100644 internal/kms/oplog_key.go create mode 100644 internal/kms/oplog_key_test.go create mode 100644 internal/kms/oplog_key_version.go create mode 100644 internal/kms/oplog_key_version_test.go create mode 100644 internal/kms/repository_oplog_key.go create mode 100644 internal/kms/repository_oplog_key_test.go create mode 100644 internal/kms/repository_oplog_key_version.go create mode 100644 internal/kms/repository_oplog_key_version_test.go create mode 100644 internal/kms/repository_session_key.go create mode 100644 internal/kms/repository_session_key_test.go create mode 100644 internal/kms/repository_session_key_version.go create mode 100644 internal/kms/repository_session_key_version_test.go create mode 100644 internal/kms/repository_token_key.go create mode 100644 internal/kms/repository_token_key_test.go create mode 100644 internal/kms/repository_token_key_version.go create mode 100644 internal/kms/repository_token_key_version_test.go create mode 100644 internal/kms/session_key.go create mode 100644 internal/kms/session_key_test.go create mode 100644 internal/kms/session_key_version.go create mode 100644 internal/kms/session_key_version_test.go create mode 100644 internal/kms/store/oplog_key.pb.go create mode 100644 internal/kms/store/session_key.pb.go create mode 100644 internal/kms/store/token_key.pb.go create mode 100644 internal/kms/token_key.go create mode 100644 internal/kms/token_key_test.go create mode 100644 internal/kms/token_key_version.go create mode 100644 internal/kms/token_key_version_test.go create mode 100644 internal/proto/local/controller/storage/kms/store/v1/oplog_key.proto create mode 100644 internal/proto/local/controller/storage/kms/store/v1/session_key.proto create mode 100644 internal/proto/local/controller/storage/kms/store/v1/token_key.proto diff --git a/Makefile b/Makefile index af3982160..647d59076 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,9 @@ protobuild: @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/kms/store/oplog_key.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/token_key.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/session_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.gen.go b/internal/db/migrations/postgres.gen.go index 01210fc48..e55927124 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -2613,39 +2613,41 @@ commit; begin; /* - ┌────────────────────────────────────────────────────────────────────────────────────────────┐ - ├────────────────────────────────────────────────────────────────┐ ○ - ├────────────────────────────────────┐ ○ ┼ - │ ○ ┼ ┌────────────────────────┐ - ┼ ┼ ┌────────────────────────┐ │ kms_session_key │ -┌────────────────────────┐ ┌────────────────────────┐ │ kms_oplog_key │ ├────────────────────────┤ -│ kms_root_key │ │ kms_database_key │ ├────────────────────────┤ │private_id │ -├────────────────────────┤ ├────────────────────────┤ │private_id │ │root_key_id │ -│private_id │ │private_id │ │root_key_id │ │ │ -│scope_id │ │root_key_id │ │ │ │ │ -│ │ │ │ │ │ │ │ -└────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ - ┼ ┼ ┼ ┼ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - ┼ ┼ ┼ ┼ - ╱│╲ ╱│╲ ╱│╲ ╱│╲ -┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ -│ kms_root_key_version │ │kms_database_key_version│ │ kms_oplog_key_version │ │kms_session_key_version │ -├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ -│private_id │ │private_id │ │private_id │ │private_id │ -│root_key_id │ │database_key_id │ │oplog_key_id │ │session_key_id │ -│key │ │root_key_id │ │root_key_id │ │root_key_id │ -│version │ │key │ │key │ │key │ -│ │ │version │ │version │ │version │ -└────────────────────────┘ └────────────────────────┘ │ │ │ │ - ┼ ┼ └────────────────────────┘ │ │ - │ ○ ┼ └────────────────────────┘ - ├────────────────────────────────────┘ ○ ┼ - ├────────────────────────────────────────────────────────────────┘ ○ - └────────────────────────────────────────────────────────────────────────────────────────────┘ + ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ + ├────────────────────────────────────────────────────────────────────────────────────────────┐ ○ + ├────────────────────────────────────────────────────────────────┐ ○ ┼ + ├────────────────────────────────────┐ ○ ┼ ┌────────────────────────┐ + │ ○ ┼ ┌────────────────────────┐ │ kms_token_key │ + ┼ ┼ ┌────────────────────────┐ │ kms_session_key │ ├────────────────────────┤ +┌────────────────────────┐ ┌────────────────────────┐ │ kms_oplog_key │ ├────────────────────────┤ │private_id │ +│ kms_root_key │ │ kms_database_key │ ├────────────────────────┤ │private_id │ │root_key_id │ +├────────────────────────┤ ├────────────────────────┤ │private_id │ │root_key_id │ │ │ +│private_id │ │private_id │ │root_key_id │ │ │ │ │ +│scope_id │ │root_key_id │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ └────────────────────────┘ +└────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ ┼ + ┼ ┼ ┼ ┼ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + ┼ ┼ ┼ ┼ │ + ╱│╲ ╱│╲ ╱│╲ ╱│╲ ┼ +┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ╱│╲ +│ kms_root_key_version │ │kms_database_key_version│ │ kms_oplog_key_version │ │kms_session_key_version │ ┌────────────────────────┐ +├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ │ kms_token_key_version │ +│private_id │ │private_id │ │private_id │ │private_id │ ├────────────────────────┤ +│root_key_id │ │database_key_id │ │oplog_key_id │ │session_key_id │ │private_id │ +│key │ │root_key_id │ │root_key_id │ │root_key_id │ │token_key_id │ +│version │ │key │ │key │ │key │ │root_key_id │ +│ │ │version │ │version │ │version │ │key │ +└────────────────────────┘ └────────────────────────┘ │ │ │ │ │version │ + ┼ ┼ └────────────────────────┘ │ │ │ │ + │ ○ ┼ └────────────────────────┘ │ │ + ├────────────────────────────────────┘ ○ ┼ └────────────────────────┘ + ├────────────────────────────────────────────────────────────────┘ ○ ┼ + ├────────────────────────────────────────────────────────────────────────────────────────────┘ ○ + └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ create table kms_root_key ( @@ -2870,6 +2872,62 @@ create trigger before insert on kms_session_key_version for each row execute procedure kms_version_column('session_key_id'); +create table kms_token_key ( + private_id wt_private_id primary key, + root_key_id wt_private_id not null unique -- there can be only one token dek per root key + references kms_root_key(private_id) + on delete cascade + on update cascade, + create_time wt_timestamp +); + + -- define the immutable fields for kms_token_key (all of them) +create trigger + immutable_columns +before +update on kms_token_key + for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time'); + +create trigger + default_create_time_column +before +insert on kms_token_key + for each row execute procedure default_create_time(); + +create table kms_token_key_version ( + private_id wt_private_id primary key, + token_key_id wt_private_id not null + references kms_token_key(private_id) + on delete cascade + on update cascade, + root_key_version_id wt_private_id not null + references kms_root_key_version(private_id) + on delete cascade + on update cascade, + version wt_version, + key bytea not null, + create_time wt_timestamp, + unique(token_key_id, version) +); + + -- define the immutable fields for kms_token_key_version (all of them) +create trigger + immutable_columns +before +update on kms_token_key_version + for each row execute procedure immutable_columns('private_id', 'token_key_id', 'root_key_version_id', 'version', 'key', 'create_time'); + +create trigger + default_create_time_column +before +insert on kms_token_key_version + for each row execute procedure default_create_time(); + +create trigger + kms_version_column +before insert on kms_token_key_version + for each row execute procedure kms_version_column('token_key_id'); + insert into oplog_ticket (name, version) values diff --git a/internal/db/migrations/postgres/31_keys.up.sql b/internal/db/migrations/postgres/31_keys.up.sql index 0fd234c24..7c72be041 100644 --- a/internal/db/migrations/postgres/31_keys.up.sql +++ b/internal/db/migrations/postgres/31_keys.up.sql @@ -1,39 +1,41 @@ begin; /* - ┌────────────────────────────────────────────────────────────────────────────────────────────┐ - ├────────────────────────────────────────────────────────────────┐ ○ - ├────────────────────────────────────┐ ○ ┼ - │ ○ ┼ ┌────────────────────────┐ - ┼ ┼ ┌────────────────────────┐ │ kms_session_key │ -┌────────────────────────┐ ┌────────────────────────┐ │ kms_oplog_key │ ├────────────────────────┤ -│ kms_root_key │ │ kms_database_key │ ├────────────────────────┤ │private_id │ -├────────────────────────┤ ├────────────────────────┤ │private_id │ │root_key_id │ -│private_id │ │private_id │ │root_key_id │ │ │ -│scope_id │ │root_key_id │ │ │ │ │ -│ │ │ │ │ │ │ │ -└────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ - ┼ ┼ ┼ ┼ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - ┼ ┼ ┼ ┼ - ╱│╲ ╱│╲ ╱│╲ ╱│╲ -┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ -│ kms_root_key_version │ │kms_database_key_version│ │ kms_oplog_key_version │ │kms_session_key_version │ -├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ -│private_id │ │private_id │ │private_id │ │private_id │ -│root_key_id │ │database_key_id │ │oplog_key_id │ │session_key_id │ -│key │ │root_key_id │ │root_key_id │ │root_key_id │ -│version │ │key │ │key │ │key │ -│ │ │version │ │version │ │version │ -└────────────────────────┘ └────────────────────────┘ │ │ │ │ - ┼ ┼ └────────────────────────┘ │ │ - │ ○ ┼ └────────────────────────┘ - ├────────────────────────────────────┘ ○ ┼ - ├────────────────────────────────────────────────────────────────┘ ○ - └────────────────────────────────────────────────────────────────────────────────────────────┘ + ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ + ├────────────────────────────────────────────────────────────────────────────────────────────┐ ○ + ├────────────────────────────────────────────────────────────────┐ ○ ┼ + ├────────────────────────────────────┐ ○ ┼ ┌────────────────────────┐ + │ ○ ┼ ┌────────────────────────┐ │ kms_token_key │ + ┼ ┼ ┌────────────────────────┐ │ kms_session_key │ ├────────────────────────┤ +┌────────────────────────┐ ┌────────────────────────┐ │ kms_oplog_key │ ├────────────────────────┤ │private_id │ +│ kms_root_key │ │ kms_database_key │ ├────────────────────────┤ │private_id │ │root_key_id │ +├────────────────────────┤ ├────────────────────────┤ │private_id │ │root_key_id │ │ │ +│private_id │ │private_id │ │root_key_id │ │ │ │ │ +│scope_id │ │root_key_id │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ └────────────────────────┘ +└────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘ ┼ + ┼ ┼ ┼ ┼ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + ┼ ┼ ┼ ┼ │ + ╱│╲ ╱│╲ ╱│╲ ╱│╲ ┼ +┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ╱│╲ +│ kms_root_key_version │ │kms_database_key_version│ │ kms_oplog_key_version │ │kms_session_key_version │ ┌────────────────────────┐ +├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ │ kms_token_key_version │ +│private_id │ │private_id │ │private_id │ │private_id │ ├────────────────────────┤ +│root_key_id │ │database_key_id │ │oplog_key_id │ │session_key_id │ │private_id │ +│key │ │root_key_id │ │root_key_id │ │root_key_id │ │token_key_id │ +│version │ │key │ │key │ │key │ │root_key_id │ +│ │ │version │ │version │ │version │ │key │ +└────────────────────────┘ └────────────────────────┘ │ │ │ │ │version │ + ┼ ┼ └────────────────────────┘ │ │ │ │ + │ ○ ┼ └────────────────────────┘ │ │ + ├────────────────────────────────────┘ ○ ┼ └────────────────────────┘ + ├────────────────────────────────────────────────────────────────┘ ○ ┼ + ├────────────────────────────────────────────────────────────────────────────────────────────┘ ○ + └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ create table kms_root_key ( @@ -258,6 +260,62 @@ create trigger before insert on kms_session_key_version for each row execute procedure kms_version_column('session_key_id'); +create table kms_token_key ( + private_id wt_private_id primary key, + root_key_id wt_private_id not null unique -- there can be only one token dek per root key + references kms_root_key(private_id) + on delete cascade + on update cascade, + create_time wt_timestamp +); + + -- define the immutable fields for kms_token_key (all of them) +create trigger + immutable_columns +before +update on kms_token_key + for each row execute procedure immutable_columns('private_id', 'root_key_id', 'create_time'); + +create trigger + default_create_time_column +before +insert on kms_token_key + for each row execute procedure default_create_time(); + +create table kms_token_key_version ( + private_id wt_private_id primary key, + token_key_id wt_private_id not null + references kms_token_key(private_id) + on delete cascade + on update cascade, + root_key_version_id wt_private_id not null + references kms_root_key_version(private_id) + on delete cascade + on update cascade, + version wt_version, + key bytea not null, + create_time wt_timestamp, + unique(token_key_id, version) +); + + -- define the immutable fields for kms_token_key_version (all of them) +create trigger + immutable_columns +before +update on kms_token_key_version + for each row execute procedure immutable_columns('private_id', 'token_key_id', 'root_key_version_id', 'version', 'key', 'create_time'); + +create trigger + default_create_time_column +before +insert on kms_token_key_version + for each row execute procedure default_create_time(); + +create trigger + kms_version_column +before insert on kms_token_key_version + for each row execute procedure kms_version_column('token_key_id'); + insert into oplog_ticket (name, version) values diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go index 9ea606a9f..d60d29485 100644 --- a/internal/iam/repository_scope.go +++ b/internal/iam/repository_scope.go @@ -14,7 +14,6 @@ import ( "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" wrapping "github.com/hashicorp/go-kms-wrapping" - "github.com/hashicorp/go-uuid" ) // CreateScope will create a scope in the repository and return the written @@ -126,10 +125,6 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o if reader == nil { reader = rand.Reader } - rootKey, err := uuid.GenerateRandomBytesWithReader(32, reader) - if err != nil { - return nil, fmt.Errorf("create scope: error generating random bytes for scope root key: %w", err) - } _, err = r.writer.DoTx( ctx, @@ -146,10 +141,10 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o s := scopeRaw.(*Scope) - // Create the scope's root key - _, _, err := kms.CreateRootKeyTx(ctx, w, externalWrappers.Root(), s.PublicId, rootKey) + // Create the scope's keys + _, err = kms.CreateKeysTx(ctx, dbr, w, externalWrappers.Root(), reader, s.PublicId) if err != nil { - return fmt.Errorf("error creating scope root key: %w", err) + return fmt.Errorf("error creating scope keys: %w", err) } kmsRepo, err := kms.NewRepository(dbr, w) diff --git a/internal/kms/const.go b/internal/kms/const.go index b85e0dc4c..96a9148f3 100644 --- a/internal/kms/const.go +++ b/internal/kms/const.go @@ -21,6 +21,9 @@ const ( // KeyPurposeTokens is used for token encryption KeyPurposeTokens + + // KeyPurposeSessions is used for session encryption + KeyPurposeSessions ) // String returns the key purpose cast as a string, just so it can be called as @@ -35,6 +38,56 @@ func (k KeyPurpose) String() string { return "recovery" case KeyPurposeTokens: return "tokens" + case KeyPurposeSessions: + return "sessions" + default: + return "unknown" + } +} + +// KeyType allows the kms repo to return a map[KeyType]Key which can be easily +// used without type casting. +type KeyType uint + +const ( + KeyTypeUnknown KeyType = iota + KeyTypeRootKey + KeyTypeRootKeyVersion + KeyTypeDatabaseKey + KeyTypeDatabaseKeyVersion + KeyTypeOplogKey + KeyTypeOplogKeyVersion + KeyTypeTokenKey + KeyTypeTokenKeyVersion + KeyTypeSessionKey + KeyTypeSessionKeyVersion +) + +// String returns the key type cast as a string, just so it can be called as +// a function instead of direct casting elsewhere, yw +func (k KeyType) String() string { + switch k { + case KeyTypeRootKey: + return "rootKey" + case KeyTypeRootKeyVersion: + return "rootKeyVersion" + case KeyTypeDatabaseKey: + return "databaseKey" + case KeyTypeDatabaseKeyVersion: + return "databaseKeyVersion" + case KeyTypeOplogKey: + return "oplogKey" + case KeyTypeOplogKeyVersion: + return "oplogKeyVersion" + case KeyTypeTokenKey: + return "tokenKey" + case KeyTypeTokenKeyVersion: + return "tokenKeyVersion" + case KeyTypeSessionKey: + return "sessionKey" + case KeyTypeSessionKeyVersion: + return "sessionKeyVersion" + default: return "unknown" } diff --git a/internal/kms/database_key.go b/internal/kms/database_key.go index b042c2327..d24f728dc 100644 --- a/internal/kms/database_key.go +++ b/internal/kms/database_key.go @@ -47,7 +47,7 @@ func (k *DatabaseKey) Clone() interface{} { } } -// VetForWrite implements db.VetForWrite() interface and validates the kms root +// VetForWrite implements db.VetForWrite() interface and validates the key // before it's written. func (k *DatabaseKey) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { if k.PrivateId == "" { diff --git a/internal/kms/ids.go b/internal/kms/ids.go index 38c2e5fcc..0a85fb18a 100644 --- a/internal/kms/ids.go +++ b/internal/kms/ids.go @@ -11,6 +11,12 @@ const ( RootKeyVersionPrefix = "krkv" DatabaseKeyPrefix = "kdk" DatabaseKeyVersionPrefix = "kdkv" + OplogKeyPrefix = "kopk" + OplogKeyVersionPrefix = "kopkv" + TokenKeyPrefix = "ktk" + TokenKeyVersionPrefix = "ktv" + SessionKeyPrefix = "ksk" + SessionKeyVersionPrefix = "kskv" ) func newRootKeyId() (string, error) { @@ -44,3 +50,51 @@ func newDatabaseKeyVersionId() (string, error) { } return id, nil } + +func newOplogKeyId() (string, error) { + id, err := db.NewPublicId(OplogKeyPrefix) + if err != nil { + return "", fmt.Errorf("new oplog key id: %w", err) + } + return id, nil +} + +func newOplogKeyVersionId() (string, error) { + id, err := db.NewPublicId(OplogKeyVersionPrefix) + if err != nil { + return "", fmt.Errorf("new oplog key version id: %w", err) + } + return id, nil +} + +func newTokenKeyId() (string, error) { + id, err := db.NewPublicId(TokenKeyPrefix) + if err != nil { + return "", fmt.Errorf("new token key id: %w", err) + } + return id, nil +} + +func newTokenKeyVersionId() (string, error) { + id, err := db.NewPublicId(TokenKeyVersionPrefix) + if err != nil { + return "", fmt.Errorf("new token key version id: %w", err) + } + return id, nil +} + +func newSessionKeyId() (string, error) { + id, err := db.NewPublicId(SessionKeyPrefix) + if err != nil { + return "", fmt.Errorf("new session key id: %w", err) + } + return id, nil +} + +func newSessionKeyVersionId() (string, error) { + id, err := db.NewPublicId(SessionKeyVersionPrefix) + if err != nil { + return "", fmt.Errorf("new session key version id: %w", err) + } + return id, nil +} diff --git a/internal/kms/ids_test.go b/internal/kms/ids_test.go index 5a2fc0288..c1d923238 100644 --- a/internal/kms/ids_test.go +++ b/internal/kms/ids_test.go @@ -30,4 +30,34 @@ func Test_Ids(t *testing.T) { require.NoError(t, err) assert.True(t, strings.HasPrefix(id, DatabaseKeyVersionPrefix+"_")) }) + t.Run("kopk", func(t *testing.T) { + id, err := newOplogKeyId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, OplogKeyPrefix+"_")) + }) + t.Run("kopkv", func(t *testing.T) { + id, err := newOplogKeyVersionId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, OplogKeyVersionPrefix+"_")) + }) + t.Run("ktk", func(t *testing.T) { + id, err := newTokenKeyId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, TokenKeyPrefix+"_")) + }) + t.Run("ktkv", func(t *testing.T) { + id, err := newTokenKeyVersionId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, TokenKeyVersionPrefix+"_")) + }) + t.Run("ksk", func(t *testing.T) { + id, err := newSessionKeyId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, SessionKeyPrefix+"_")) + }) + t.Run("kskv", func(t *testing.T) { + id, err := newSessionKeyVersionId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, SessionKeyVersionPrefix+"_")) + }) } diff --git a/internal/kms/kms.go b/internal/kms/kms.go index 1639650a4..9b6f2fb99 100644 --- a/internal/kms/kms.go +++ b/internal/kms/kms.go @@ -149,7 +149,7 @@ func generateKeyId(scopeId string, purpose KeyPurpose, version uint32) string { // decryption. func (k *Kms) GetWrapper(ctx context.Context, scopeId string, purpose KeyPurpose, opt ...Option) (wrapping.Wrapper, error) { switch purpose { - case KeyPurposeOplog, KeyPurposeDatabase, KeyPurposeTokens: + case KeyPurposeOplog, KeyPurposeDatabase, KeyPurposeTokens, KeyPurposeSessions: case KeyPurposeUnknown: return nil, errors.New("key purpose not specified") default: diff --git a/internal/kms/oplog_key.go b/internal/kms/oplog_key.go new file mode 100644 index 000000000..dd7377954 --- /dev/null +++ b/internal/kms/oplog_key.go @@ -0,0 +1,77 @@ +package kms + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultOplogKeyTableName = "kms_oplog_key" +) + +type OplogKey struct { + *store.OplogKey + tableName string `gorm:"-"` +} + +// NewOplogKey creates a new in memory key. No options +// are currently supported. +func NewOplogKey(rootKeyId string, opt ...Option) (*OplogKey, error) { + if rootKeyId == "" { + return nil, fmt.Errorf("new root key: missing root key id: %w", db.ErrInvalidParameter) + } + c := &OplogKey{ + OplogKey: &store.OplogKey{ + RootKeyId: rootKeyId, + }, + } + return c, nil +} + +// AllocOplogKey will allocate a key +func AllocOplogKey() OplogKey { + return OplogKey{ + OplogKey: &store.OplogKey{}, + } +} + +// Clone creates a clone of the key +func (k *OplogKey) Clone() interface{} { + cp := proto.Clone(k.OplogKey) + return &OplogKey{ + OplogKey: cp.(*store.OplogKey), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the key +// before it's written. +func (k *OplogKey) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("oplog key vet for write: missing private id: %w", db.ErrInvalidParameter) + } + if opType == db.CreateOp { + if k.RootKeyId == "" { + return fmt.Errorf("oplog key vet for write: missing root key id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *OplogKey) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultOplogKeyTableName +} + +// 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 *OplogKey) SetTableName(n string) { + k.tableName = n +} diff --git a/internal/kms/oplog_key_test.go b/internal/kms/oplog_key_test.go new file mode 100644 index 000000000..e4197b604 --- /dev/null +++ b/internal/kms/oplog_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 OplogKey attributes are +// immutable and those tests are covered by TestOplogKey_ImmutableFields + +func TestOplogKey_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.OplogKey + 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.OplogKey { + k := kms.AllocOplogKey() + 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.AllocOplogKey()).Error) + got, err := kms.NewOplogKey(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.OplogKeyPrefix) + 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 TestOplogKey_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.AllocOplogKey()).Error) + + tests := []struct { + name string + key *kms.OplogKey + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestOplogKey(t, conn, rk.PrivateId), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.OplogKey { + k := kms.AllocOplogKey() + id, err := db.NewPublicId(kms.OplogKeyPrefix) + 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.AllocOplogKey() + 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.AllocOplogKey() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestOplogKey_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.TestOplogKey(t, conn, rk.PrivateId) + cp := k.Clone() + assert.True(proto.Equal(cp.(*kms.OplogKey).OplogKey, k.OplogKey)) + }) + 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.TestOplogKey(t, conn, rk.PrivateId) + k2 := kms.TestOplogKey(t, conn, rk2.PrivateId) + + cp := k.Clone() + assert.True(!proto.Equal(cp.(*kms.OplogKey).OplogKey, k2.OplogKey)) + }) +} + +func TestOplogKey_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultOplogKeyTableName + 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.AllocOplogKey() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocOplogKey() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/kms/oplog_key_version.go b/internal/kms/oplog_key_version.go new file mode 100644 index 000000000..a61284f55 --- /dev/null +++ b/internal/kms/oplog_key_version.go @@ -0,0 +1,114 @@ +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 ( + DefaultOplogKeyVersionTableName = "kms_oplog_key_version" +) + +type OplogKeyVersion struct { + *store.OplogKeyVersion + tableName string `gorm:"-"` +} + +// OplogKeyVersion creates a new in memory key version. No options are +// currently supported. +func NewOplogKeyVersion(oplogKeyId string, key []byte, rootKeyVersionId string, opt ...Option) (*OplogKeyVersion, error) { + if oplogKeyId == "" { + return nil, fmt.Errorf("new oplog key version: missing oplog key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("new oplog key version: missing key: %w", db.ErrInvalidParameter) + } + if rootKeyVersionId == "" { + return nil, fmt.Errorf("new oplog key version: missing root key version id: %w", db.ErrInvalidParameter) + } + + k := &OplogKeyVersion{ + OplogKeyVersion: &store.OplogKeyVersion{ + OplogKeyId: oplogKeyId, + RootKeyVersionId: rootKeyVersionId, + Key: key, + }, + } + return k, nil +} + +// AllocOplogKeyVersion allocates a key version +func AllocOplogKeyVersion() OplogKeyVersion { + return OplogKeyVersion{ + OplogKeyVersion: &store.OplogKeyVersion{}, + } +} + +// Clone creates a clone of the key version +func (k *OplogKeyVersion) Clone() interface{} { + cp := proto.Clone(k.OplogKeyVersion) + return &OplogKeyVersion{ + OplogKeyVersion: cp.(*store.OplogKeyVersion), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the key +// version before it's written. +func (k *OplogKeyVersion) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("oplog key version vet for write: missing private id: %w", db.ErrInvalidParameter) + } + if opType == db.CreateOp { + if k.CtKey == nil { + return fmt.Errorf("oplog key version vet for write: missing key: %w", db.ErrInvalidParameter) + } + if k.OplogKeyId == "" { + return fmt.Errorf("oplog key version vet for write: missing oplog key id: %w", db.ErrInvalidParameter) + } + if k.RootKeyVersionId == "" { + return fmt.Errorf("oplog key version vet for write: missing root key version id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *OplogKeyVersion) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultOplogKeyVersionTableName +} + +// 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 *OplogKeyVersion) SetTableName(n string) { + k.tableName = n +} + +// Encrypt will encrypt the key version's key +func (k *OplogKeyVersion) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.OplogKeyVersion directly + if err := structwrapping.WrapStruct(ctx, cipher, k.OplogKeyVersion, nil); err != nil { + return fmt.Errorf("error encrypting kms oplog key version: %w", err) + } + return nil +} + +// Decrypt will decrypt the key version's key +func (k *OplogKeyVersion) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.OplogKeyVersion directly + if err := structwrapping.UnwrapStruct(ctx, cipher, k.OplogKeyVersion, nil); err != nil { + return fmt.Errorf("error decrypting kms oplog key version: %w", err) + } + return nil +} diff --git a/internal/kms/oplog_key_version_test.go b/internal/kms/oplog_key_version_test.go new file mode 100644 index 000000000..8282a5fa7 --- /dev/null +++ b/internal/kms/oplog_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 OplogKeyVersion attributes are +// immutable and those tests are covered by TestOplogKey_ImmutableFields + +func TestOplogKeyVersion_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.TestOplogKey(t, conn, rk.PrivateId) + + type args struct { + oplogKeyId string + key []byte + rootKeyVersionId string + opt []kms.Option + } + tests := []struct { + name string + args args + want *kms.OplogKeyVersion + wantErr bool + wantIsErr error + create bool + wantCreateErr bool + }{ + { + name: "empty-id", + args: args{ + key: []byte("test key"), + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-key", + args: args{ + oplogKeyId: dk.PrivateId, + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-root-key-version-id", + args: args{ + oplogKeyId: dk.PrivateId, + key: []byte("test key"), + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "valid", + args: args{ + oplogKeyId: dk.PrivateId, + key: []byte("test key"), + rootKeyVersionId: rkv.PrivateId, + }, + want: func() *kms.OplogKeyVersion { + k := kms.AllocOplogKeyVersion() + k.RootKeyVersionId = rkv.PrivateId + k.Key = []byte("test key") + k.OplogKeyId = 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.AllocOplogKeyVersion()).Error) + got, err := kms.NewOplogKeyVersion(tt.args.oplogKeyId, 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.OplogKeyPrefix) + 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 TestOplogKeyVersion_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) + opk := kms.TestOplogKey(t, conn, rk.PrivateId) + + require.NoError(t, conn.Where("1=1").Delete(kms.AllocOplogKeyVersion()).Error) + + tests := []struct { + name string + key *kms.OplogKeyVersion + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestOplogKeyVersion(t, conn, rkvWrapper, opk.PrivateId, []byte("test key")), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.OplogKeyVersion { + k := kms.AllocOplogKeyVersion() + id, err := db.NewPublicId(kms.OplogKeyVersionPrefix) + 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.AllocOplogKeyVersion() + 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.AllocOplogKeyVersion() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestOplogKeyVersion_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.TestOplogKey(t, conn, rk.PrivateId) + kv := kms.TestOplogKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test key")) + cp := kv.Clone() + assert.True(proto.Equal(cp.(*kms.OplogKeyVersion).OplogKeyVersion, kv.OplogKeyVersion)) + }) + 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.TestOplogKey(t, conn, rk.PrivateId) + k2 := kms.TestOplogKey(t, conn, rk2.PrivateId) + kv := kms.TestOplogKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test key")) + kv2 := kms.TestOplogKeyVersion(t, conn, rkvWrapper2, k2.PrivateId, []byte("test key 2")) + + cp := kv.Clone() + assert.True(!proto.Equal(cp.(*kms.OplogKeyVersion).OplogKeyVersion, kv2.OplogKeyVersion)) + }) +} + +func TestOplogKeyVersion_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultOplogKeyVersionTableName + 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.AllocOplogKeyVersion() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocOplogKeyVersion() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/kms/repository.go b/internal/kms/repository.go index c5e8e410e..7576e43d9 100644 --- a/internal/kms/repository.go +++ b/internal/kms/repository.go @@ -3,8 +3,13 @@ package kms import ( "context" "errors" + "fmt" + "io" "github.com/hashicorp/boundary/internal/db" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/wrappers/aead" + "github.com/hashicorp/go-uuid" ) // Repository is the iam database repository @@ -57,3 +62,110 @@ func (r *Repository) list(ctx context.Context, resources interface{}, where stri func (r *Repository) DefaultLimit() int { return r.defaultLimit } + +// KeyIder defines a common interface for all keys returned from CreateKeysTx in +// a Keys map +type KeyIder interface { + GetPrivateId() string +} + +// Keys defines a return type for CreateKeysTx so the returned keys can be +// easily accessed via their KeyType +type Keys map[KeyType]KeyIder + +// CreateKeysTx creates the root key and DEKs returns a map of the new keys This +// function encapsulates all the work required within a db.TxHandler and allows +// this capability to be shared with the iam repo. +func CreateKeysTx(ctx context.Context, dbReader db.Reader, dbWriter db.Writer, rootWrapper wrapping.Wrapper, randomReader io.Reader, scopeId string) (Keys, error) { + if dbReader == nil { + return nil, fmt.Errorf("create keys: missing db reader: %w", db.ErrInvalidParameter) + } + if dbWriter == nil { + return nil, fmt.Errorf("create keys: missing db writer: %w", db.ErrInvalidParameter) + } + if rootWrapper == nil { + return nil, fmt.Errorf("create keys: missing root wrapper: %w", db.ErrInvalidParameter) + } + if randomReader == nil { + return nil, fmt.Errorf("create keys: missing random reader: %w", db.ErrInvalidParameter) + } + if scopeId == "" { + return nil, fmt.Errorf("create keys: missing scope id: %w", db.ErrInvalidParameter) + } + k, err := generateKey(randomReader) + if err != nil { + return nil, fmt.Errorf("create keys: error generating random bytes for root key in scope %s: %w", scopeId, err) + } + rootKey, rootKeyVersion, err := createRootKeyTx(ctx, dbWriter, rootWrapper, scopeId, k) + if err != nil { + return nil, fmt.Errorf("create keys: unable to create root key in scope %s: %w", scopeId, err) + } + + rkvWrapper := aead.NewWrapper(nil) + if _, err := rkvWrapper.SetConfig(map[string]string{ + "key_id": rootKeyVersion.GetPrivateId(), + }); err != nil { + return nil, fmt.Errorf("create keys: error setting config on aead root wrapper in scope %s: %w", scopeId, err) + } + if err := rkvWrapper.SetAESGCMKeyBytes(rootKeyVersion.GetKey()); err != nil { + return nil, fmt.Errorf("create keys: error setting key bytes on aead root wrapper in scope %s: %w", scopeId, err) + } + + k, err = generateKey(randomReader) + if err != nil { + return nil, fmt.Errorf("create keys: error generating random bytes for database key in scope %s: %w", scopeId, err) + } + dbKey, dbKeyVersion, err := createDatabaseKeyTx(ctx, dbReader, dbWriter, rkvWrapper, k) + if err != nil { + return nil, fmt.Errorf("create keys: unable to create database key in scope %s: %w", scopeId, err) + } + + k, err = generateKey(randomReader) + if err != nil { + return nil, fmt.Errorf("create keys: error generating random bytes for oplog key in scope %s: %w", scopeId, err) + } + oplogKey, oplogKeyVersion, err := createOplogKeyTx(ctx, dbReader, dbWriter, rkvWrapper, k) + if err != nil { + return nil, fmt.Errorf("create keys: unable to create oplog key in scope %s: %w", scopeId, err) + } + + k, err = generateKey(randomReader) + if err != nil { + return nil, fmt.Errorf("create keys: error generating random bytes for session key in scope %s: %w", scopeId, err) + } + sessionKey, sessionKeyVersion, err := createSessionKeyTx(ctx, dbReader, dbWriter, rkvWrapper, k) + if err != nil { + return nil, fmt.Errorf("create keys: unable to create session key in scope %s: %w", scopeId, err) + } + + k, err = generateKey(randomReader) + if err != nil { + return nil, fmt.Errorf("create keys: error generating random bytes for token key in scope %s: %w", scopeId, err) + } + tokenKey, tokenKeyVersion, err := createTokenKeyTx(ctx, dbReader, dbWriter, rkvWrapper, k) + if err != nil { + return nil, fmt.Errorf("create keys: unable to create token key in scope %s: %w", scopeId, err) + } + + keys := Keys{ + KeyTypeRootKey: rootKey, + KeyTypeRootKeyVersion: rootKeyVersion, + KeyTypeDatabaseKey: dbKey, + KeyTypeDatabaseKeyVersion: dbKeyVersion, + KeyTypeOplogKey: oplogKey, + KeyTypeOplogKeyVersion: oplogKeyVersion, + KeyTypeSessionKey: sessionKey, + KeyTypeSessionKeyVersion: sessionKeyVersion, + KeyTypeTokenKey: tokenKey, + KeyTypeTokenKeyVersion: tokenKeyVersion, + } + return keys, nil +} + +func generateKey(randomReader io.Reader) ([]byte, error) { + k, err := uuid.GenerateRandomBytesWithReader(32, randomReader) + if err != nil { + return nil, fmt.Errorf("error generating random bytes for key: %w", err) + } + return k, nil +} diff --git a/internal/kms/repository_database_key.go b/internal/kms/repository_database_key.go index 0be1f7672..86233d4ba 100644 --- a/internal/kms/repository_database_key.go +++ b/internal/kms/repository_database_key.go @@ -19,7 +19,7 @@ func (r *Repository) CreateDatabaseKey(ctx context.Context, rkvWrapper wrapping. 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 { + if returnedDk, returnedDv, err = createDatabaseKeyTx(ctx, reader, w, rkvWrapper, key); err != nil { return err } return nil @@ -31,10 +31,10 @@ func (r *Repository) CreateDatabaseKey(ctx context.Context, rkvWrapper wrapping. return returnedDk.(*DatabaseKey), returnedDv.(*DatabaseKeyVersion), err } -// CreateDatabaseKeyTx inserts into the db (via db.Writer) and returns the new database key +// 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) { +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) } diff --git a/internal/kms/repository_database_key_test.go b/internal/kms/repository_database_key_test.go index 50a957b28..5f36000ee 100644 --- a/internal/kms/repository_database_key_test.go +++ b/internal/kms/repository_database_key_test.go @@ -283,8 +283,8 @@ func TestRepository_ListDatabaseKeys(t *testing.T) { 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) + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(conn.Where("scope_id in(?)", []interface{}{org.PublicId, proj.PublicId}).Delete(kms.AllocRootKey()).Error) rk := kms.TestRootKey(t, conn, proj.PublicId) kms.TestDatabaseKey(t, conn, rk.PrivateId) require.NoError(err) diff --git a/internal/kms/repository_oplog_key.go b/internal/kms/repository_oplog_key.go new file mode 100644 index 000000000..5e2bd8cad --- /dev/null +++ b/internal/kms/repository_oplog_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" +) + +// CreateOplogKey inserts into the repository and returns the new key and the +// key version. There are no valid options at this time. +func (r *Repository) CreateOplogKey(ctx context.Context, rkvWrapper wrapping.Wrapper, key []byte, opt ...Option) (*OplogKey, *OplogKeyVersion, 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 = createOplogKeyTx(ctx, reader, w, rkvWrapper, key); err != nil { + return err + } + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("create oplog key: %w", err) + } + return returnedDk.(*OplogKey), returnedDv.(*OplogKeyVersion), err +} + +// createOplogKeyTx inserts into the db (via db.Writer) and returns the new key +// and the 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 createOplogKeyTx(ctx context.Context, r db.Reader, w db.Writer, rkvWrapper wrapping.Wrapper, key []byte) (*OplogKey, *OplogKeyVersion, error) { + if rkvWrapper == nil { + return nil, nil, fmt.Errorf("create oplog key: missing key wrapper: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, nil, fmt.Errorf("create oplog key: missing key: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, nil, fmt.Errorf("create oplog 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 oplog 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 oplog key: unable to lookup root key version %s: %w", rootKeyVersionId, err) + } + + opk := AllocOplogKey() + opv := AllocOplogKeyVersion() + id, err := newOplogKeyId() + if err != nil { + return nil, nil, fmt.Errorf("create oplog key: %w", err) + } + opk.PrivateId = id + opk.RootKeyId = rv.RootKeyId + + id, err = newOplogKeyVersionId() + if err != nil { + return nil, nil, fmt.Errorf("create oplog key: %w", err) + } + opv.PrivateId = id + opv.OplogKeyId = opk.PrivateId + opv.RootKeyVersionId = rootKeyVersionId + opv.Key = key + if err := opv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, nil, fmt.Errorf("create oplog key: %w", err) + } + + // no oplog entries for keys + if err := w.Create(ctx, &opk); err != nil { + return nil, nil, fmt.Errorf("create oplog key: key create: %w", err) + } + // no oplog entries for key versions + if err := w.Create(ctx, &opv); err != nil { + return nil, nil, fmt.Errorf("create oplog key: version create: %w", err) + } + + return &opk, &opv, err +} + +// LookupOplogKey will look up a key in the repository. If the key is not +// found, it will return nil, nil. +func (r *Repository) LookupOplogKey(ctx context.Context, privateId string, opt ...Option) (*OplogKey, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup oplog key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocOplogKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup oplog key: failed %w for %s", err, privateId) + } + return &k, nil +} + +// DeleteOplogKey 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) DeleteOplogKey(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete oplog key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocOplogKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete oplog 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 oplog key: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// ListOplogKeys will list the keys. Supports the WithLimit option. +func (r *Repository) ListOplogKeys(ctx context.Context, opt ...Option) ([]*OplogKey, error) { + var keys []*OplogKey + err := r.list(ctx, &keys, "1=1", nil, opt...) + if err != nil { + return nil, fmt.Errorf("list oplog keys: %w", err) + } + return keys, nil +} diff --git a/internal/kms/repository_oplog_key_test.go b/internal/kms/repository_oplog_key_test.go new file mode 100644 index 000000000..bdda78f55 --- /dev/null +++ b/internal/kms/repository_oplog_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_CreateOplogKey(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) + opk, opv, err := repo.CreateOplogKey(context.Background(), tt.args.keyWrapper, tt.args.key, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Nil(opk) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + return + } + require.NoError(err) + assert.NotNil(opk.CreateTime) + foundKey, err := repo.LookupOplogKey(context.Background(), opk.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKey, opk)) + + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, opk.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(opv.CreateTime) + foundKeyVersion, err := repo.LookupOplogKeyVersion(context.Background(), tt.args.keyWrapper, opv.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKeyVersion, opv)) + + // make sure there was no oplog written + err = db.TestVerifyOplog(t, rw, opv.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_DeleteOplogKey(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.OplogKey + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestOplogKey(t, conn, rk.PrivateId), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.OplogKey { + k := kms.AllocOplogKey() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.OplogKey { + id, err := db.NewPublicId(kms.RootKeyPrefix) + require.NoError(t, err) + k := kms.AllocOplogKey() + 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.DeleteOplogKey(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.LookupOplogKey(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_ListOplogKeys(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++ { + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(conn.Where("scope_id in(?)", []interface{}{org.PublicId, proj.PublicId}).Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, proj.PublicId) + kms.TestOplogKey(t, conn, rk.PrivateId) + require.NoError(err) + } + got, err := repo.ListOplogKeys(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_oplog_key_version.go b/internal/kms/repository_oplog_key_version.go new file mode 100644 index 000000000..12b9b808f --- /dev/null +++ b/internal/kms/repository_oplog_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" +) + +// CreateOplogKeyVersion inserts into the repository and returns the new key +// version with its PrivateId. There are no valid options at this time. +func (r *Repository) CreateOplogKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, oplogKeyId string, key []byte, opt ...Option) (*OplogKeyVersion, error) { + if rkvWrapper == nil { + return nil, fmt.Errorf("create oplog key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, fmt.Errorf("create oplog 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 oplog key version: missing root key version id: %w", db.ErrInvalidParameter) + } + if oplogKeyId == "" { + return nil, fmt.Errorf("create oplog key version: missing oplog key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("create oplog key version: missing key: %w", db.ErrInvalidParameter) + } + kv := AllocOplogKeyVersion() + id, err := newOplogKeyVersionId() + if err != nil { + return nil, fmt.Errorf("create oplog key version: %w", err) + } + kv.PrivateId = id + kv.RootKeyVersionId = rootKeyVersionId + kv.Key = key + kv.OplogKeyId = oplogKeyId + if err := kv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("create oplog 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 oplog key version: %w for %s oplog key id", err, kv.OplogKeyId) + } + return returnedKey.(*OplogKeyVersion), err +} + +// LookupOplogKeyVersion will look up a key version in the repository. If +// the key version is not found, it will return nil, nil. +func (r *Repository) LookupOplogKeyVersion(ctx context.Context, keyWrapper wrapping.Wrapper, privateId string, opt ...Option) (*OplogKeyVersion, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup oplog key version: missing private id: %w", db.ErrInvalidParameter) + } + if keyWrapper == nil { + return nil, fmt.Errorf("lookup oplog key version: missing key wrapper: %w", db.ErrInvalidParameter) + } + k := AllocOplogKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup oplog key version: failed %w for %s", err, privateId) + } + if err := k.Decrypt(ctx, keyWrapper); err != nil { + return nil, fmt.Errorf("lookup oplog key version: decrypt: %w", err) + } + return &k, nil +} + +// DeleteOplogKeyVersion 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) DeleteOplogKeyVersion(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete oplog key version: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocOplogKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete oplog 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 oplog key version: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// LatestOplogKeyVersion searches for the key version with the highest +// version number. When no results are found, it returns nil, +// db.ErrRecordNotFound. +func (r *Repository) LatestOplogKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, oplogKeyId string, opt ...Option) (*OplogKeyVersion, error) { + if oplogKeyId == "" { + return nil, fmt.Errorf("latest oplog key version: missing oplog key id: %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("latest oplog key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var foundKeys []*OplogKeyVersion + if err := r.reader.SearchWhere(ctx, &foundKeys, "oplog_key_id = ?", []interface{}{oplogKeyId}, db.WithLimit(1), db.WithOrder("version desc")); err != nil { + return nil, fmt.Errorf("latest oplog key version: failed %w for %s", err, oplogKeyId) + } + if len(foundKeys) == 0 { + return nil, db.ErrRecordNotFound + } + if err := foundKeys[0].Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("latest oplog key version: %w", err) + } + return foundKeys[0], nil +} + +// ListOplogKeyVersions will lists versions of a key. Supports the WithLimit option. +func (r *Repository) ListOplogKeyVersions(ctx context.Context, rkvWrapper wrapping.Wrapper, oplogKeyId string, opt ...Option) ([]*OplogKeyVersion, error) { + if oplogKeyId == "" { + return nil, fmt.Errorf("list oplog key versions: missing oplog key id %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("list oplog key versions: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var versions []*OplogKeyVersion + err := r.list(ctx, &versions, "oplog_key_id = ?", []interface{}{oplogKeyId}, opt...) + if err != nil { + return nil, fmt.Errorf("list oplog key versions: %w", err) + } + for i, k := range versions { + if err := k.Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("list oplog key versions: error decrypting key num %d: %w", i, err) + } + } + return versions, nil +} diff --git a/internal/kms/repository_oplog_key_version_test.go b/internal/kms/repository_oplog_key_version_test.go new file mode 100644 index 000000000..088009f11 --- /dev/null +++ b/internal/kms/repository_oplog_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_CreateOplogKeyVersion(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.TestOplogKey(t, conn, rk.PrivateId) + + type args struct { + key []byte + oplogKeyId 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, + oplogKeyId: dk.PrivateId, + }, + wantErr: false, + }, + { + name: "invalid-rkv-wrapper", + args: args{ + key: []byte("test key"), + keyWrapper: wrapper, + oplogKeyId: dk.PrivateId, + }, + wantErr: true, + }, + { + name: "empty-key", + args: args{ + key: nil, + keyWrapper: wrapper, + oplogKeyId: 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.CreateOplogKeyVersion(context.Background(), tt.args.keyWrapper, tt.args.oplogKeyId, 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.LookupOplogKeyVersion(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_DeleteOplogKeyVersion(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.TestOplogKey(t, conn, rk.PrivateId) + + type args struct { + key *kms.OplogKeyVersion + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestOplogKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("oplog key")), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.OplogKeyVersion { + k := kms.AllocOplogKeyVersion() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.OplogKeyVersion { + id, err := db.NewPublicId(kms.OplogKeyPrefix) + require.NoError(t, err) + k := kms.AllocOplogKeyVersion() + 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.DeleteOplogKeyVersion(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.LookupOplogKeyVersion(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_LatestOplogKeyVersion(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.TestOplogKey(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.AllocOplogKeyVersion()).Error) + testKeys := []*kms.OplogKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestOplogKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("test oplog key")) + testKeys = append(testKeys, k) + } + assert.Equal(tt.createCnt, len(testKeys)) + got, err := repo.LatestOplogKeyVersion(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_ListOplogKeyVersions(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.TestOplogKey(t, conn, rk.PrivateId) + + type args struct { + oplogKeyId 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{ + oplogKeyId: 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, + oplogKeyId: dk.PrivateId, + }, + wantCnt: repo.DefaultLimit(), + wantErr: false, + }, + { + name: "custom-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + keyWrapper: rkvWrapper, + oplogKeyId: dk.PrivateId, + opt: []kms.Option{kms.WithLimit(3)}, + }, + wantCnt: 3, + wantErr: false, + }, + { + name: "bad-org", + createCnt: 1, + args: args{ + keyWrapper: rkvWrapper, + oplogKeyId: "bad-id", + }, + wantCnt: 0, + wantErr: false, + }, + { + name: "nil-wrapper", + createCnt: 1, + args: args{ + keyWrapper: nil, + oplogKeyId: 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.AllocOplogKeyVersion()).Error) + keyVersions := []*kms.OplogKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestOplogKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("oplog key")) + keyVersions = append(keyVersions, k) + } + assert.Equal(tt.createCnt, len(keyVersions)) + got, err := repo.ListOplogKeyVersions(context.Background(), tt.args.keyWrapper, tt.args.oplogKeyId, 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 1ff91284a..642c08d9f 100644 --- a/internal/kms/repository_root_key.go +++ b/internal/kms/repository_root_key.go @@ -18,7 +18,7 @@ func (r *Repository) CreateRootKey(ctx context.Context, keyWrapper wrapping.Wrap db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error { var err error - if returnedRk, returnedKv, err = CreateRootKeyTx(ctx, w, keyWrapper, scopeId, key); err != nil { + if returnedRk, returnedKv, err = createRootKeyTx(ctx, w, keyWrapper, scopeId, key); err != nil { return err } return nil @@ -30,10 +30,10 @@ func (r *Repository) CreateRootKey(ctx context.Context, keyWrapper wrapping.Wrap return returnedRk.(*RootKey), returnedKv.(*RootKeyVersion), err } -// CreateRootKeyTx inserts into the db (via db.Writer) and returns the new root key +// 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. -func CreateRootKeyTx(ctx context.Context, w db.Writer, keyWrapper wrapping.Wrapper, scopeId string, key []byte) (*RootKey, *RootKeyVersion, error) { +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_session_key.go b/internal/kms/repository_session_key.go new file mode 100644 index 000000000..1f9ee8dee --- /dev/null +++ b/internal/kms/repository_session_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" +) + +// CreateSessionKey inserts into the repository and returns the new key and the +// key version. There are no valid options at this time. +func (r *Repository) CreateSessionKey(ctx context.Context, rkvWrapper wrapping.Wrapper, key []byte, opt ...Option) (*SessionKey, *SessionKeyVersion, 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 = createSessionKeyTx(ctx, reader, w, rkvWrapper, key); err != nil { + return err + } + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("create session key: %w", err) + } + return returnedDk.(*SessionKey), returnedDv.(*SessionKeyVersion), err +} + +// createSessionKeyTx inserts into the db (via db.Writer) and returns the new key +// and the 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 createSessionKeyTx(ctx context.Context, r db.Reader, w db.Writer, rkvWrapper wrapping.Wrapper, key []byte) (*SessionKey, *SessionKeyVersion, error) { + if rkvWrapper == nil { + return nil, nil, fmt.Errorf("create session key: missing key wrapper: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, nil, fmt.Errorf("create session key: missing key: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, nil, fmt.Errorf("create session 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 session 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 session key: unable to lookup root key version %s: %w", rootKeyVersionId, err) + } + + tk := AllocSessionKey() + tv := AllocSessionKeyVersion() + id, err := newSessionKeyId() + if err != nil { + return nil, nil, fmt.Errorf("create session key: %w", err) + } + tk.PrivateId = id + tk.RootKeyId = rv.RootKeyId + + id, err = newSessionKeyVersionId() + if err != nil { + return nil, nil, fmt.Errorf("create session key: %w", err) + } + tv.PrivateId = id + tv.SessionKeyId = tk.PrivateId + tv.RootKeyVersionId = rootKeyVersionId + tv.Key = key + if err := tv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, nil, fmt.Errorf("create session key: %w", err) + } + + // no session entries for keys + if err := w.Create(ctx, &tk); err != nil { + return nil, nil, fmt.Errorf("create session key: key create: %w", err) + } + // no session entries for key versions + if err := w.Create(ctx, &tv); err != nil { + return nil, nil, fmt.Errorf("create session key: version create: %w", err) + } + + return &tk, &tv, err +} + +// LookupSessionKey will look up a key in the repository. If the key is not +// found, it will return nil, nil. +func (r *Repository) LookupSessionKey(ctx context.Context, privateId string, opt ...Option) (*SessionKey, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup session key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocSessionKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup session key: failed %w for %s", err, privateId) + } + return &k, nil +} + +// DeleteSessionKey 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) DeleteSessionKey(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete session key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocSessionKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete session 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 session 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 session key: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// ListSessionKeys will list the keys. Supports the WithLimit option. +func (r *Repository) ListSessionKeys(ctx context.Context, opt ...Option) ([]*SessionKey, error) { + var keys []*SessionKey + err := r.list(ctx, &keys, "1=1", nil, opt...) + if err != nil { + return nil, fmt.Errorf("list session keys: %w", err) + } + return keys, nil +} diff --git a/internal/kms/repository_session_key_test.go b/internal/kms/repository_session_key_test.go new file mode 100644 index 000000000..1d2260ee6 --- /dev/null +++ b/internal/kms/repository_session_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_CreateSessionKey(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) + tk, tv, err := repo.CreateSessionKey(context.Background(), tt.args.keyWrapper, tt.args.key, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Nil(tk) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + return + } + require.NoError(err) + assert.NotNil(tk.CreateTime) + foundKey, err := repo.LookupSessionKey(context.Background(), tk.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKey, tk)) + + // make sure there was no session written + err = db.TestVerifyOplog(t, rw, tk.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(tv.CreateTime) + foundKeyVersion, err := repo.LookupSessionKeyVersion(context.Background(), tt.args.keyWrapper, tv.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKeyVersion, tv)) + + // make sure there was no session written + err = db.TestVerifyOplog(t, rw, tv.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_DeleteSessionKey(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.SessionKey + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestSessionKey(t, conn, rk.PrivateId), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.SessionKey { + k := kms.AllocSessionKey() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.SessionKey { + id, err := db.NewPublicId(kms.RootKeyPrefix) + require.NoError(t, err) + k := kms.AllocSessionKey() + 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.DeleteSessionKey(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 session 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.LookupSessionKey(context.Background(), tt.args.key.PrivateId) + assert.Error(err) + assert.Nil(foundKey) + assert.True(errors.Is(err, db.ErrRecordNotFound)) + + // make sure there was no session 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_ListSessionKeys(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++ { + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(conn.Where("scope_id in(?)", []interface{}{org.PublicId, proj.PublicId}).Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, proj.PublicId) + kms.TestSessionKey(t, conn, rk.PrivateId) + require.NoError(err) + } + got, err := repo.ListSessionKeys(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_session_key_version.go b/internal/kms/repository_session_key_version.go new file mode 100644 index 000000000..ef1ba42e4 --- /dev/null +++ b/internal/kms/repository_session_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" +) + +// CreateSessionKeyVersion inserts into the repository and returns the new key +// version with its PrivateId. There are no valid options at this time. +func (r *Repository) CreateSessionKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, sessionKeyId string, key []byte, opt ...Option) (*SessionKeyVersion, error) { + if rkvWrapper == nil { + return nil, fmt.Errorf("create session key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, fmt.Errorf("create session 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 session key version: missing root key version id: %w", db.ErrInvalidParameter) + } + if sessionKeyId == "" { + return nil, fmt.Errorf("create session key version: missing session key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("create session key version: missing key: %w", db.ErrInvalidParameter) + } + kv := AllocSessionKeyVersion() + id, err := newSessionKeyVersionId() + if err != nil { + return nil, fmt.Errorf("create session key version: %w", err) + } + kv.PrivateId = id + kv.RootKeyVersionId = rootKeyVersionId + kv.Key = key + kv.SessionKeyId = sessionKeyId + if err := kv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("create session 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 session 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 session key version: %w for %s session key id", err, kv.SessionKeyId) + } + return returnedKey.(*SessionKeyVersion), err +} + +// LookupSessionKeyVersion will look up a key version in the repository. If +// the key version is not found, it will return nil, nil. +func (r *Repository) LookupSessionKeyVersion(ctx context.Context, keyWrapper wrapping.Wrapper, privateId string, opt ...Option) (*SessionKeyVersion, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup session key version: missing private id: %w", db.ErrInvalidParameter) + } + if keyWrapper == nil { + return nil, fmt.Errorf("lookup session key version: missing key wrapper: %w", db.ErrInvalidParameter) + } + k := AllocSessionKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup session key version: failed %w for %s", err, privateId) + } + if err := k.Decrypt(ctx, keyWrapper); err != nil { + return nil, fmt.Errorf("lookup session key version: decrypt: %w", err) + } + return &k, nil +} + +// DeleteSessionKeyVersion 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) DeleteSessionKeyVersion(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete session key version: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocSessionKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete session 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 session 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 session key version: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// LatestSessionKeyVersion searches for the key version with the highest +// version number. When no results are found, it returns nil, +// db.ErrRecordNotFound. +func (r *Repository) LatestSessionKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, sessionKeyId string, opt ...Option) (*SessionKeyVersion, error) { + if sessionKeyId == "" { + return nil, fmt.Errorf("latest session key version: missing session key id: %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("latest session key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var foundKeys []*SessionKeyVersion + if err := r.reader.SearchWhere(ctx, &foundKeys, "session_key_id = ?", []interface{}{sessionKeyId}, db.WithLimit(1), db.WithOrder("version desc")); err != nil { + return nil, fmt.Errorf("latest session key version: failed %w for %s", err, sessionKeyId) + } + if len(foundKeys) == 0 { + return nil, db.ErrRecordNotFound + } + if err := foundKeys[0].Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("latest session key version: %w", err) + } + return foundKeys[0], nil +} + +// ListSessionKeyVersions will lists versions of a key. Supports the WithLimit option. +func (r *Repository) ListSessionKeyVersions(ctx context.Context, rkvWrapper wrapping.Wrapper, sessionKeyId string, opt ...Option) ([]*SessionKeyVersion, error) { + if sessionKeyId == "" { + return nil, fmt.Errorf("list session key versions: missing session key id %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("list session key versions: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var versions []*SessionKeyVersion + err := r.list(ctx, &versions, "session_key_id = ?", []interface{}{sessionKeyId}, opt...) + if err != nil { + return nil, fmt.Errorf("list session key versions: %w", err) + } + for i, k := range versions { + if err := k.Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("list session key versions: error decrypting key num %d: %w", i, err) + } + } + return versions, nil +} diff --git a/internal/kms/repository_session_key_version_test.go b/internal/kms/repository_session_key_version_test.go new file mode 100644 index 000000000..764af7f51 --- /dev/null +++ b/internal/kms/repository_session_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_CreateSessionKeyVersion(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) + sk := kms.TestSessionKey(t, conn, rk.PrivateId) + + type args struct { + key []byte + sessionKeyId 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, + sessionKeyId: sk.PrivateId, + }, + wantErr: false, + }, + { + name: "invalid-rkv-wrapper", + args: args{ + key: []byte("test key"), + keyWrapper: wrapper, + sessionKeyId: sk.PrivateId, + }, + wantErr: true, + }, + { + name: "empty-key", + args: args{ + key: nil, + keyWrapper: wrapper, + sessionKeyId: sk.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.CreateSessionKeyVersion(context.Background(), tt.args.keyWrapper, tt.args.sessionKeyId, 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.LookupSessionKeyVersion(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_DeleteSessionKeyVersion(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) + sk := kms.TestSessionKey(t, conn, rk.PrivateId) + + type args struct { + key *kms.SessionKeyVersion + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestSessionKeyVersion(t, conn, rkvWrapper, sk.PrivateId, []byte("session key")), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.SessionKeyVersion { + k := kms.AllocSessionKeyVersion() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.SessionKeyVersion { + id, err := db.NewPublicId(kms.SessionKeyPrefix) + require.NoError(t, err) + k := kms.AllocSessionKeyVersion() + 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.DeleteSessionKeyVersion(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.LookupSessionKeyVersion(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_LatestSessionKeyVersion(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) + sk := kms.TestSessionKey(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.AllocSessionKeyVersion()).Error) + testKeys := []*kms.SessionKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestSessionKeyVersion(t, conn, rkvWrapper, sk.PrivateId, []byte("test key")) + testKeys = append(testKeys, k) + } + assert.Equal(tt.createCnt, len(testKeys)) + got, err := repo.LatestSessionKeyVersion(context.Background(), tt.keyWrapper, sk.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_ListSessionKeyVersions(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) + sk := kms.TestSessionKey(t, conn, rk.PrivateId) + + type args struct { + sessionKeyId 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{ + sessionKeyId: sk.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, + sessionKeyId: sk.PrivateId, + }, + wantCnt: repo.DefaultLimit(), + wantErr: false, + }, + { + name: "custom-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + keyWrapper: rkvWrapper, + sessionKeyId: sk.PrivateId, + opt: []kms.Option{kms.WithLimit(3)}, + }, + wantCnt: 3, + wantErr: false, + }, + { + name: "bad-org", + createCnt: 1, + args: args{ + keyWrapper: rkvWrapper, + sessionKeyId: "bad-id", + }, + wantCnt: 0, + wantErr: false, + }, + { + name: "nil-wrapper", + createCnt: 1, + args: args{ + keyWrapper: nil, + sessionKeyId: sk.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.AllocSessionKeyVersion()).Error) + keyVersions := []*kms.SessionKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestSessionKeyVersion(t, conn, rkvWrapper, sk.PrivateId, []byte("session key")) + keyVersions = append(keyVersions, k) + } + assert.Equal(tt.createCnt, len(keyVersions)) + got, err := repo.ListSessionKeyVersions(context.Background(), tt.args.keyWrapper, tt.args.sessionKeyId, 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_test.go b/internal/kms/repository_test.go index 9e1373582..1f9be0ba4 100644 --- a/internal/kms/repository_test.go +++ b/internal/kms/repository_test.go @@ -1,10 +1,16 @@ package kms_test import ( + "context" + "crypto/rand" + "errors" + "io" "testing" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -72,3 +78,174 @@ func TestNewRepository(t *testing.T) { }) } } + +func TestCreateKeysTx(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) + + type args struct { + ctx context.Context + dbReader db.Reader + dbWriter db.Writer + rootWrapper wrapping.Wrapper + randomReader io.Reader + scopeId string + } + tests := []struct { + name string + args args + wantErr bool + wantErrIs error + }{ + { + name: "valid", + args: args{ + dbReader: rw, + dbWriter: rw, + rootWrapper: wrapper, + randomReader: rand.Reader, + scopeId: org.PublicId, + }, + }, + { + name: "nil-reader", + args: args{ + dbReader: nil, + dbWriter: rw, + rootWrapper: wrapper, + randomReader: rand.Reader, + scopeId: org.PublicId, + }, + wantErr: true, + wantErrIs: db.ErrInvalidParameter, + }, + { + name: "nil-writer", + args: args{ + dbReader: rw, + dbWriter: nil, + rootWrapper: wrapper, + randomReader: rand.Reader, + scopeId: org.PublicId, + }, + wantErr: true, + wantErrIs: db.ErrInvalidParameter, + }, + { + name: "nil-wrapper", + args: args{ + dbReader: rw, + dbWriter: rw, + rootWrapper: nil, + randomReader: rand.Reader, + scopeId: org.PublicId, + }, + wantErr: true, + wantErrIs: db.ErrInvalidParameter, + }, + { + name: "empty-scope", + args: args{ + dbReader: rw, + dbWriter: rw, + rootWrapper: wrapper, + randomReader: rand.Reader, + scopeId: "", + }, + wantErr: true, + wantErrIs: db.ErrInvalidParameter, + }, + { + name: "bad-scope", + args: args{ + dbReader: rw, + dbWriter: rw, + rootWrapper: wrapper, + randomReader: rand.Reader, + scopeId: "o_thisIsAnInvalidScopeId", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + keys, err := kms.CreateKeysTx(tt.args.ctx, tt.args.dbReader, tt.args.dbWriter, tt.args.rootWrapper, tt.args.randomReader, tt.args.scopeId) + if tt.wantErr { + require.Error(err) + if tt.wantErrIs != nil { + assert.Truef(errors.Is(err, tt.wantErrIs), "unexpected error: %s", err.Error()) + } + return + } + require.NoError(err) + rk := kms.AllocRootKey() + rk.PrivateId = keys[kms.KeyTypeRootKey].GetPrivateId() + err = rw.LookupById(context.Background(), &rk) + require.NoError(err) + assert.Equal(rk.ScopeId, tt.args.scopeId) + + rkv := kms.AllocRootKeyVersion() + rkv.PrivateId = keys[kms.KeyTypeRootKeyVersion].GetPrivateId() + err = rw.LookupById(context.Background(), &rkv) + require.NoError(err) + assert.Equal(rkv.RootKeyId, rk.PrivateId) + + dk := kms.AllocDatabaseKey() + dk.PrivateId = keys[kms.KeyTypeDatabaseKey].GetPrivateId() + err = rw.LookupById(context.Background(), &dk) + require.NoError(err) + assert.Equal(rk.PrivateId, dk.RootKeyId) + + dkv := kms.AllocDatabaseKeyVersion() + dkv.PrivateId = keys[kms.KeyTypeDatabaseKeyVersion].GetPrivateId() + err = rw.LookupById(context.Background(), &dkv) + require.NoError(err) + assert.Equal(dk.PrivateId, dkv.DatabaseKeyId) + assert.Equal(rkv.PrivateId, dkv.RootKeyVersionId) + + opk := kms.AllocOplogKey() + opk.PrivateId = keys[kms.KeyTypeOplogKey].GetPrivateId() + err = rw.LookupById(context.Background(), &opk) + require.NoError(err) + assert.Equal(rk.PrivateId, opk.RootKeyId) + + opkv := kms.AllocOplogKeyVersion() + opkv.PrivateId = keys[kms.KeyTypeOplogKeyVersion].GetPrivateId() + err = rw.LookupById(context.Background(), &opkv) + require.NoError(err) + assert.Equal(opk.PrivateId, opkv.OplogKeyId) + assert.Equal(rkv.PrivateId, opkv.RootKeyVersionId) + + sk := kms.AllocSessionKey() + sk.PrivateId = keys[kms.KeyTypeSessionKey].GetPrivateId() + err = rw.LookupById(context.Background(), &sk) + require.NoError(err) + assert.Equal(rk.PrivateId, sk.RootKeyId) + + skv := kms.AllocSessionKeyVersion() + skv.PrivateId = keys[kms.KeyTypeSessionKeyVersion].GetPrivateId() + err = rw.LookupById(context.Background(), &skv) + require.NoError(err) + assert.Equal(sk.PrivateId, skv.SessionKeyId) + assert.Equal(rkv.PrivateId, skv.RootKeyVersionId) + + tk := kms.AllocTokenKey() + tk.PrivateId = keys[kms.KeyTypeTokenKey].GetPrivateId() + err = rw.LookupById(context.Background(), &tk) + require.NoError(err) + assert.Equal(rk.PrivateId, tk.RootKeyId) + + tkv := kms.AllocTokenKeyVersion() + tkv.PrivateId = keys[kms.KeyTypeTokenKeyVersion].GetPrivateId() + err = rw.LookupById(context.Background(), &tkv) + require.NoError(err) + assert.Equal(tk.PrivateId, tkv.TokenKeyId) + assert.Equal(rkv.PrivateId, tkv.RootKeyVersionId) + }) + } +} diff --git a/internal/kms/repository_token_key.go b/internal/kms/repository_token_key.go new file mode 100644 index 000000000..9fabe2c07 --- /dev/null +++ b/internal/kms/repository_token_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" +) + +// CreateTokenKey inserts into the repository and returns the new key and the +// key version. There are no valid options at this time. +func (r *Repository) CreateTokenKey(ctx context.Context, rkvWrapper wrapping.Wrapper, key []byte, opt ...Option) (*TokenKey, *TokenKeyVersion, 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 = createTokenKeyTx(ctx, reader, w, rkvWrapper, key); err != nil { + return err + } + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("create token key: %w", err) + } + return returnedDk.(*TokenKey), returnedDv.(*TokenKeyVersion), err +} + +// createTokenKeyTx inserts into the db (via db.Writer) and returns the new key +// and the 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 createTokenKeyTx(ctx context.Context, r db.Reader, w db.Writer, rkvWrapper wrapping.Wrapper, key []byte) (*TokenKey, *TokenKeyVersion, error) { + if rkvWrapper == nil { + return nil, nil, fmt.Errorf("create token key: missing key wrapper: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, nil, fmt.Errorf("create token key: missing key: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, nil, fmt.Errorf("create token 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 token 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 token key: unable to lookup root key version %s: %w", rootKeyVersionId, err) + } + + tk := AllocTokenKey() + tv := AllocTokenKeyVersion() + id, err := newTokenKeyId() + if err != nil { + return nil, nil, fmt.Errorf("create token key: %w", err) + } + tk.PrivateId = id + tk.RootKeyId = rv.RootKeyId + + id, err = newTokenKeyVersionId() + if err != nil { + return nil, nil, fmt.Errorf("create token key: %w", err) + } + tv.PrivateId = id + tv.TokenKeyId = tk.PrivateId + tv.RootKeyVersionId = rootKeyVersionId + tv.Key = key + if err := tv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, nil, fmt.Errorf("create token key: %w", err) + } + + // no token entries for keys + if err := w.Create(ctx, &tk); err != nil { + return nil, nil, fmt.Errorf("create token key: key create: %w", err) + } + // no token entries for key versions + if err := w.Create(ctx, &tv); err != nil { + return nil, nil, fmt.Errorf("create token key: version create: %w", err) + } + + return &tk, &tv, err +} + +// LookupTokenKey will look up a key in the repository. If the key is not +// found, it will return nil, nil. +func (r *Repository) LookupTokenKey(ctx context.Context, privateId string, opt ...Option) (*TokenKey, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup token key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocTokenKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup token key: failed %w for %s", err, privateId) + } + return &k, nil +} + +// DeleteTokenKey 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) DeleteTokenKey(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete token key: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocTokenKey() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete token 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 token 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 token key: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// ListTokenKeys will list the keys. Supports the WithLimit option. +func (r *Repository) ListTokenKeys(ctx context.Context, opt ...Option) ([]*TokenKey, error) { + var keys []*TokenKey + err := r.list(ctx, &keys, "1=1", nil, opt...) + if err != nil { + return nil, fmt.Errorf("list token keys: %w", err) + } + return keys, nil +} diff --git a/internal/kms/repository_token_key_test.go b/internal/kms/repository_token_key_test.go new file mode 100644 index 000000000..de3435052 --- /dev/null +++ b/internal/kms/repository_token_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_CreateTokenKey(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) + tk, tv, err := repo.CreateTokenKey(context.Background(), tt.args.keyWrapper, tt.args.key, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Nil(tk) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + return + } + require.NoError(err) + assert.NotNil(tk.CreateTime) + foundKey, err := repo.LookupTokenKey(context.Background(), tk.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKey, tk)) + + // make sure there was no token written + err = db.TestVerifyOplog(t, rw, tk.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(tv.CreateTime) + foundKeyVersion, err := repo.LookupTokenKeyVersion(context.Background(), tt.args.keyWrapper, tv.PrivateId) + assert.NoError(err) + assert.True(proto.Equal(foundKeyVersion, tv)) + + // make sure there was no token written + err = db.TestVerifyOplog(t, rw, tv.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_DeleteTokenKey(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.TokenKey + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestTokenKey(t, conn, rk.PrivateId), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.TokenKey { + k := kms.AllocTokenKey() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.TokenKey { + id, err := db.NewPublicId(kms.RootKeyPrefix) + require.NoError(t, err) + k := kms.AllocTokenKey() + 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.DeleteTokenKey(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 token 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.LookupTokenKey(context.Background(), tt.args.key.PrivateId) + assert.Error(err) + assert.Nil(foundKey) + assert.True(errors.Is(err, db.ErrRecordNotFound)) + + // make sure there was no token 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_ListTokenKeys(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++ { + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NoError(conn.Where("scope_id in(?)", []interface{}{org.PublicId, proj.PublicId}).Delete(kms.AllocRootKey()).Error) + rk := kms.TestRootKey(t, conn, proj.PublicId) + kms.TestTokenKey(t, conn, rk.PrivateId) + require.NoError(err) + } + got, err := repo.ListTokenKeys(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_token_key_version.go b/internal/kms/repository_token_key_version.go new file mode 100644 index 000000000..8a6e71a5d --- /dev/null +++ b/internal/kms/repository_token_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" +) + +// CreateTokenKeyVersion inserts into the repository and returns the new key +// version with its PrivateId. There are no valid options at this time. +func (r *Repository) CreateTokenKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, tokenKeyId string, key []byte, opt ...Option) (*TokenKeyVersion, error) { + if rkvWrapper == nil { + return nil, fmt.Errorf("create token key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + rootKeyVersionId := rkvWrapper.KeyID() + switch { + case !strings.HasPrefix(rootKeyVersionId, RootKeyVersionPrefix): + return nil, fmt.Errorf("create token 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 token key version: missing root key version id: %w", db.ErrInvalidParameter) + } + if tokenKeyId == "" { + return nil, fmt.Errorf("create token key version: missing token key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("create token key version: missing key: %w", db.ErrInvalidParameter) + } + kv := AllocTokenKeyVersion() + id, err := newTokenKeyVersionId() + if err != nil { + return nil, fmt.Errorf("create token key version: %w", err) + } + kv.PrivateId = id + kv.RootKeyVersionId = rootKeyVersionId + kv.Key = key + kv.TokenKeyId = tokenKeyId + if err := kv.Encrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("create token 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 token 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 token key version: %w for %s token key id", err, kv.TokenKeyId) + } + return returnedKey.(*TokenKeyVersion), err +} + +// LookupTokenKeyVersion will look up a key version in the repository. If +// the key version is not found, it will return nil, nil. +func (r *Repository) LookupTokenKeyVersion(ctx context.Context, keyWrapper wrapping.Wrapper, privateId string, opt ...Option) (*TokenKeyVersion, error) { + if privateId == "" { + return nil, fmt.Errorf("lookup token key version: missing private id: %w", db.ErrInvalidParameter) + } + if keyWrapper == nil { + return nil, fmt.Errorf("lookup token key version: missing key wrapper: %w", db.ErrInvalidParameter) + } + k := AllocTokenKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return nil, fmt.Errorf("lookup token key version: failed %w for %s", err, privateId) + } + if err := k.Decrypt(ctx, keyWrapper); err != nil { + return nil, fmt.Errorf("lookup token key version: decrypt: %w", err) + } + return &k, nil +} + +// DeleteTokenKeyVersion 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) DeleteTokenKeyVersion(ctx context.Context, privateId string, opt ...Option) (int, error) { + if privateId == "" { + return db.NoRowsAffected, fmt.Errorf("delete token key version: missing private id: %w", db.ErrInvalidParameter) + } + k := AllocTokenKeyVersion() + k.PrivateId = privateId + if err := r.reader.LookupById(ctx, &k); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete token 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 token 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 token key version: %s: %w", privateId, err) + } + return rowsDeleted, nil +} + +// LatestTokenKeyVersion searches for the key version with the highest +// version number. When no results are found, it returns nil, +// db.ErrRecordNotFound. +func (r *Repository) LatestTokenKeyVersion(ctx context.Context, rkvWrapper wrapping.Wrapper, tokenKeyId string, opt ...Option) (*TokenKeyVersion, error) { + if tokenKeyId == "" { + return nil, fmt.Errorf("latest token key version: missing token key id: %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("latest token key version: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var foundKeys []*TokenKeyVersion + if err := r.reader.SearchWhere(ctx, &foundKeys, "token_key_id = ?", []interface{}{tokenKeyId}, db.WithLimit(1), db.WithOrder("version desc")); err != nil { + return nil, fmt.Errorf("latest token key version: failed %w for %s", err, tokenKeyId) + } + if len(foundKeys) == 0 { + return nil, db.ErrRecordNotFound + } + if err := foundKeys[0].Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("latest token key version: %w", err) + } + return foundKeys[0], nil +} + +// ListTokenKeyVersions will lists versions of a key. Supports the WithLimit option. +func (r *Repository) ListTokenKeyVersions(ctx context.Context, rkvWrapper wrapping.Wrapper, tokenKeyId string, opt ...Option) ([]*TokenKeyVersion, error) { + if tokenKeyId == "" { + return nil, fmt.Errorf("list token key versions: missing token key id %w", db.ErrInvalidParameter) + } + if rkvWrapper == nil { + return nil, fmt.Errorf("list token key versions: missing root key version wrapper: %w", db.ErrInvalidParameter) + } + var versions []*TokenKeyVersion + err := r.list(ctx, &versions, "token_key_id = ?", []interface{}{tokenKeyId}, opt...) + if err != nil { + return nil, fmt.Errorf("list token key versions: %w", err) + } + for i, k := range versions { + if err := k.Decrypt(ctx, rkvWrapper); err != nil { + return nil, fmt.Errorf("list token key versions: error decrypting key num %d: %w", i, err) + } + } + return versions, nil +} diff --git a/internal/kms/repository_token_key_version_test.go b/internal/kms/repository_token_key_version_test.go new file mode 100644 index 000000000..333a2ef7d --- /dev/null +++ b/internal/kms/repository_token_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_CreateTokenKeyVersion(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.TestTokenKey(t, conn, rk.PrivateId) + + type args struct { + key []byte + tokenKeyId 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, + tokenKeyId: dk.PrivateId, + }, + wantErr: false, + }, + { + name: "invalid-rkv-wrapper", + args: args{ + key: []byte("test key"), + keyWrapper: wrapper, + tokenKeyId: dk.PrivateId, + }, + wantErr: true, + }, + { + name: "empty-key", + args: args{ + key: nil, + keyWrapper: wrapper, + tokenKeyId: 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.CreateTokenKeyVersion(context.Background(), tt.args.keyWrapper, tt.args.tokenKeyId, 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.LookupTokenKeyVersion(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_DeleteTokenKeyVersion(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.TestTokenKey(t, conn, rk.PrivateId) + + type args struct { + key *kms.TokenKeyVersion + opt []kms.Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsError error + }{ + { + name: "valid", + args: args{ + key: kms.TestTokenKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("token key")), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-private-id", + args: args{ + key: func() *kms.TokenKeyVersion { + k := kms.AllocTokenKeyVersion() + return &k + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + key: func() *kms.TokenKeyVersion { + id, err := db.NewPublicId(kms.TokenKeyPrefix) + require.NoError(t, err) + k := kms.AllocTokenKeyVersion() + 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.DeleteTokenKeyVersion(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.LookupTokenKeyVersion(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_LatestTokenKeyVersion(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.TestTokenKey(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.AllocTokenKeyVersion()).Error) + testKeys := []*kms.TokenKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestTokenKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("test key")) + testKeys = append(testKeys, k) + } + assert.Equal(tt.createCnt, len(testKeys)) + got, err := repo.LatestTokenKeyVersion(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_ListTokenKeyVersions(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.TestTokenKey(t, conn, rk.PrivateId) + + type args struct { + tokenKeyId 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{ + tokenKeyId: 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, + tokenKeyId: dk.PrivateId, + }, + wantCnt: repo.DefaultLimit(), + wantErr: false, + }, + { + name: "custom-limit", + createCnt: repo.DefaultLimit() + 1, + args: args{ + keyWrapper: rkvWrapper, + tokenKeyId: dk.PrivateId, + opt: []kms.Option{kms.WithLimit(3)}, + }, + wantCnt: 3, + wantErr: false, + }, + { + name: "bad-org", + createCnt: 1, + args: args{ + keyWrapper: rkvWrapper, + tokenKeyId: "bad-id", + }, + wantCnt: 0, + wantErr: false, + }, + { + name: "nil-wrapper", + createCnt: 1, + args: args{ + keyWrapper: nil, + tokenKeyId: 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.AllocTokenKeyVersion()).Error) + keyVersions := []*kms.TokenKeyVersion{} + for i := 0; i < tt.createCnt; i++ { + k := kms.TestTokenKeyVersion(t, conn, rkvWrapper, dk.PrivateId, []byte("token key")) + keyVersions = append(keyVersions, k) + } + assert.Equal(tt.createCnt, len(keyVersions)) + got, err := repo.ListTokenKeyVersions(context.Background(), tt.args.keyWrapper, tt.args.tokenKeyId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got)) + }) + } +} diff --git a/internal/kms/root_key.go b/internal/kms/root_key.go index 2f53fa66d..96fac745f 100644 --- a/internal/kms/root_key.go +++ b/internal/kms/root_key.go @@ -49,7 +49,7 @@ func (k *RootKey) Clone() interface{} { } } -// VetForWrite implements db.VetForWrite() interface and validates the kms root +// VetForWrite implements db.VetForWrite() interface and validates the key // before it's written. func (k *RootKey) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { if k.PrivateId == "" { diff --git a/internal/kms/session_key.go b/internal/kms/session_key.go new file mode 100644 index 000000000..6ca6215a3 --- /dev/null +++ b/internal/kms/session_key.go @@ -0,0 +1,77 @@ +package kms + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultSessionKeyTableName = "kms_session_key" +) + +type SessionKey struct { + *store.SessionKey + tableName string `gorm:"-"` +} + +// NewSessionKey creates a new in memory key. No options +// are currently supported. +func NewSessionKey(rootKeyId string, opt ...Option) (*SessionKey, error) { + if rootKeyId == "" { + return nil, fmt.Errorf("new root key: missing root key id: %w", db.ErrInvalidParameter) + } + c := &SessionKey{ + SessionKey: &store.SessionKey{ + RootKeyId: rootKeyId, + }, + } + return c, nil +} + +// AllocSessionKey will allocate a key +func AllocSessionKey() SessionKey { + return SessionKey{ + SessionKey: &store.SessionKey{}, + } +} + +// Clone creates a clone of the key +func (k *SessionKey) Clone() interface{} { + cp := proto.Clone(k.SessionKey) + return &SessionKey{ + SessionKey: cp.(*store.SessionKey), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the key +// before it's written. +func (k *SessionKey) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("session key vet for write: missing private id: %w", db.ErrInvalidParameter) + } + if opType == db.CreateOp { + if k.RootKeyId == "" { + return fmt.Errorf("session key vet for write: missing root key id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *SessionKey) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultSessionKeyTableName +} + +// 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 *SessionKey) SetTableName(n string) { + k.tableName = n +} diff --git a/internal/kms/session_key_test.go b/internal/kms/session_key_test.go new file mode 100644 index 000000000..2385010ca --- /dev/null +++ b/internal/kms/session_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 SessionKey attributes are +// immutable and those tests are covered by TestSessionKey_ImmutableFields + +func TestSessionKey_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.SessionKey + 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.SessionKey { + k := kms.AllocSessionKey() + 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.AllocSessionKey()).Error) + got, err := kms.NewSessionKey(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.SessionKeyPrefix) + 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 TestSessionKey_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.AllocSessionKey()).Error) + + tests := []struct { + name string + key *kms.SessionKey + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestSessionKey(t, conn, rk.PrivateId), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.SessionKey { + k := kms.AllocSessionKey() + id, err := db.NewPublicId(kms.SessionKeyPrefix) + 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.AllocSessionKey() + 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.AllocSessionKey() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestSessionKey_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.TestSessionKey(t, conn, rk.PrivateId) + cp := k.Clone() + assert.True(proto.Equal(cp.(*kms.SessionKey).SessionKey, k.SessionKey)) + }) + 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.TestSessionKey(t, conn, rk.PrivateId) + k2 := kms.TestSessionKey(t, conn, rk2.PrivateId) + + cp := k.Clone() + assert.True(!proto.Equal(cp.(*kms.SessionKey).SessionKey, k2.SessionKey)) + }) +} + +func TestSessionKey_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultSessionKeyTableName + 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.AllocSessionKey() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocSessionKey() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/kms/session_key_version.go b/internal/kms/session_key_version.go new file mode 100644 index 000000000..c26e2e688 --- /dev/null +++ b/internal/kms/session_key_version.go @@ -0,0 +1,114 @@ +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 ( + DefaultSessionKeyVersionTableName = "kms_session_key_version" +) + +type SessionKeyVersion struct { + *store.SessionKeyVersion + tableName string `gorm:"-"` +} + +// SessionKeyVersion creates a new in memory key version. No options are +// currently supported. +func NewSessionKeyVersion(sessionKeyId string, key []byte, rootKeyVersionId string, opt ...Option) (*SessionKeyVersion, error) { + if sessionKeyId == "" { + return nil, fmt.Errorf("new session key version: missing session key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("new session key version: missing key: %w", db.ErrInvalidParameter) + } + if rootKeyVersionId == "" { + return nil, fmt.Errorf("new session key version: missing root key version id: %w", db.ErrInvalidParameter) + } + + k := &SessionKeyVersion{ + SessionKeyVersion: &store.SessionKeyVersion{ + SessionKeyId: sessionKeyId, + RootKeyVersionId: rootKeyVersionId, + Key: key, + }, + } + return k, nil +} + +// AllocSessionKeyVersion allocates a key version +func AllocSessionKeyVersion() SessionKeyVersion { + return SessionKeyVersion{ + SessionKeyVersion: &store.SessionKeyVersion{}, + } +} + +// Clone creates a clone of the key version +func (k *SessionKeyVersion) Clone() interface{} { + cp := proto.Clone(k.SessionKeyVersion) + return &SessionKeyVersion{ + SessionKeyVersion: cp.(*store.SessionKeyVersion), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the key +// version before it's written. +func (k *SessionKeyVersion) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("session key version vet for write: missing private id: %w", db.ErrInvalidParameter) + } + if opType == db.CreateOp { + if k.CtKey == nil { + return fmt.Errorf("session key version vet for write: missing key: %w", db.ErrInvalidParameter) + } + if k.SessionKeyId == "" { + return fmt.Errorf("session key version vet for write: missing session key id: %w", db.ErrInvalidParameter) + } + if k.RootKeyVersionId == "" { + return fmt.Errorf("session key version vet for write: missing root key version id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *SessionKeyVersion) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultSessionKeyVersionTableName +} + +// 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 *SessionKeyVersion) SetTableName(n string) { + k.tableName = n +} + +// Encrypt will encrypt the key version's key +func (k *SessionKeyVersion) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.SessionKeyVersion directly + if err := structwrapping.WrapStruct(ctx, cipher, k.SessionKeyVersion, nil); err != nil { + return fmt.Errorf("error encrypting kms session key version: %w", err) + } + return nil +} + +// Decrypt will decrypt the key version's key +func (k *SessionKeyVersion) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.SessionKeyVersion directly + if err := structwrapping.UnwrapStruct(ctx, cipher, k.SessionKeyVersion, nil); err != nil { + return fmt.Errorf("error decrypting kms session key version: %w", err) + } + return nil +} diff --git a/internal/kms/session_key_version_test.go b/internal/kms/session_key_version_test.go new file mode 100644 index 000000000..fc53dcbd4 --- /dev/null +++ b/internal/kms/session_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 essionKeyVersion attributes are +// immutable and those tests are covered by TestSessionKey_ImmutableFields + +func TestSessionKeyVersion_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.TestSessionKey(t, conn, rk.PrivateId) + + type args struct { + sessionKeyId string + key []byte + rootKeyVersionId string + opt []kms.Option + } + tests := []struct { + name string + args args + want *kms.SessionKeyVersion + wantErr bool + wantIsErr error + create bool + wantCreateErr bool + }{ + { + name: "empty-id", + args: args{ + key: []byte("test key"), + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-key", + args: args{ + sessionKeyId: dk.PrivateId, + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-root-key-version-id", + args: args{ + sessionKeyId: dk.PrivateId, + key: []byte("test key"), + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "valid", + args: args{ + sessionKeyId: dk.PrivateId, + key: []byte("test key"), + rootKeyVersionId: rkv.PrivateId, + }, + want: func() *kms.SessionKeyVersion { + k := kms.AllocSessionKeyVersion() + k.RootKeyVersionId = rkv.PrivateId + k.Key = []byte("test key") + k.SessionKeyId = 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.AllocSessionKeyVersion()).Error) + got, err := kms.NewSessionKeyVersion(tt.args.sessionKeyId, 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.SessionKeyPrefix) + 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 TestSessionKeyVersion_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) + tk := kms.TestSessionKey(t, conn, rk.PrivateId) + + require.NoError(t, conn.Where("1=1").Delete(kms.AllocSessionKeyVersion()).Error) + + tests := []struct { + name string + key *kms.SessionKeyVersion + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestSessionKeyVersion(t, conn, rkvWrapper, tk.PrivateId, []byte("test key")), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.SessionKeyVersion { + k := kms.AllocSessionKeyVersion() + id, err := db.NewPublicId(kms.SessionKeyVersionPrefix) + 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.AllocSessionKeyVersion() + 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.AllocSessionKeyVersion() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestSessionKeyVersion_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.TestSessionKey(t, conn, rk.PrivateId) + kv := kms.TestSessionKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test key")) + cp := kv.Clone() + assert.True(proto.Equal(cp.(*kms.SessionKeyVersion).SessionKeyVersion, kv.SessionKeyVersion)) + }) + 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.TestSessionKey(t, conn, rk.PrivateId) + k2 := kms.TestSessionKey(t, conn, rk2.PrivateId) + kv := kms.TestSessionKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test key")) + kv2 := kms.TestSessionKeyVersion(t, conn, rkvWrapper2, k2.PrivateId, []byte("test key 2")) + + cp := kv.Clone() + assert.True(!proto.Equal(cp.(*kms.SessionKeyVersion).SessionKeyVersion, kv2.SessionKeyVersion)) + }) +} + +func TestSessionKeyVersion_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultSessionKeyVersionTableName + 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.AllocSessionKeyVersion() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocSessionKeyVersion() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/kms/store/oplog_key.pb.go b/internal/kms/store/oplog_key.pb.go new file mode 100644 index 000000000..62403ec3e --- /dev/null +++ b/internal/kms/store/oplog_key.pb.go @@ -0,0 +1,334 @@ +// 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/oplog_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 OplogKey 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 *OplogKey) Reset() { + *x = OplogKey{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_oplog_key_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OplogKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OplogKey) ProtoMessage() {} + +func (x *OplogKey) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_oplog_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 OplogKey.ProtoReflect.Descriptor instead. +func (*OplogKey) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_oplog_key_proto_rawDescGZIP(), []int{0} +} + +func (x *OplogKey) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *OplogKey) GetRootKeyId() string { + if x != nil { + return x.RootKeyId + } + return "" +} + +func (x *OplogKey) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +type OplogKeyVersion 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"` + // id for the key version + // @inject_tag: `gorm:"default:null"` + OplogKeyId string `protobuf:"bytes,20,opt,name=oplog_key_id,json=oplogKeyId,proto3" json:"oplog_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 *OplogKeyVersion) Reset() { + *x = OplogKeyVersion{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_oplog_key_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OplogKeyVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OplogKeyVersion) ProtoMessage() {} + +func (x *OplogKeyVersion) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_oplog_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 OplogKeyVersion.ProtoReflect.Descriptor instead. +func (*OplogKeyVersion) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_oplog_key_proto_rawDescGZIP(), []int{1} +} + +func (x *OplogKeyVersion) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *OplogKeyVersion) GetOplogKeyId() string { + if x != nil { + return x.OplogKeyId + } + return "" +} + +func (x *OplogKeyVersion) GetRootKeyVersionId() string { + if x != nil { + return x.RootKeyVersionId + } + return "" +} + +func (x *OplogKeyVersion) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *OplogKeyVersion) GetCtKey() []byte { + if x != nil { + return x.CtKey + } + return nil +} + +func (x *OplogKeyVersion) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *OplogKeyVersion) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +var File_controller_storage_kms_store_v1_oplog_key_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_oplog_key_proto_rawDesc = []byte{ + 0x0a, 0x2f, 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, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 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, + 0x96, 0x01, 0x0a, 0x08, 0x4f, 0x70, 0x6c, 0x6f, 0x67, 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, 0x91, 0x02, 0x0a, 0x0f, 0x4f, 0x70, 0x6c, + 0x6f, 0x67, 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, 0x20, 0x0a, 0x0c, 0x6f, + 0x70, 0x6c, 0x6f, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x6f, 0x70, 0x6c, 0x6f, 0x67, 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_oplog_key_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_oplog_key_proto_rawDescData = file_controller_storage_kms_store_v1_oplog_key_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_oplog_key_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_oplog_key_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_oplog_key_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_oplog_key_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_oplog_key_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_oplog_key_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_kms_store_v1_oplog_key_proto_goTypes = []interface{}{ + (*OplogKey)(nil), // 0: controller.storage.kms.store.v1.OplogKey + (*OplogKeyVersion)(nil), // 1: controller.storage.kms.store.v1.OplogKeyVersion + (*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_kms_store_v1_oplog_key_proto_depIdxs = []int32{ + 2, // 0: controller.storage.kms.store.v1.OplogKey.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // 1: controller.storage.kms.store.v1.OplogKeyVersion.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_oplog_key_proto_init() } +func file_controller_storage_kms_store_v1_oplog_key_proto_init() { + if File_controller_storage_kms_store_v1_oplog_key_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_oplog_key_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OplogKey); 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_oplog_key_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OplogKeyVersion); 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_oplog_key_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_oplog_key_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_oplog_key_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_oplog_key_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_oplog_key_proto = out.File + file_controller_storage_kms_store_v1_oplog_key_proto_rawDesc = nil + file_controller_storage_kms_store_v1_oplog_key_proto_goTypes = nil + file_controller_storage_kms_store_v1_oplog_key_proto_depIdxs = nil +} diff --git a/internal/kms/store/session_key.pb.go b/internal/kms/store/session_key.pb.go new file mode 100644 index 000000000..06431f5f6 --- /dev/null +++ b/internal/kms/store/session_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/session_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 SessionKey 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 *SessionKey) Reset() { + *x = SessionKey{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_session_key_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SessionKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionKey) ProtoMessage() {} + +func (x *SessionKey) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_session_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 SessionKey.ProtoReflect.Descriptor instead. +func (*SessionKey) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_session_key_proto_rawDescGZIP(), []int{0} +} + +func (x *SessionKey) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *SessionKey) GetRootKeyId() string { + if x != nil { + return x.RootKeyId + } + return "" +} + +func (x *SessionKey) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +type SessionKeyVersion 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"` + // id for the key version + // @inject_tag: `gorm:"default:null"` + SessionKeyId string `protobuf:"bytes,20,opt,name=session_key_id,json=sessionKeyId,proto3" json:"session_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 *SessionKeyVersion) Reset() { + *x = SessionKeyVersion{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_session_key_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SessionKeyVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionKeyVersion) ProtoMessage() {} + +func (x *SessionKeyVersion) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_session_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 SessionKeyVersion.ProtoReflect.Descriptor instead. +func (*SessionKeyVersion) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_session_key_proto_rawDescGZIP(), []int{1} +} + +func (x *SessionKeyVersion) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *SessionKeyVersion) GetSessionKeyId() string { + if x != nil { + return x.SessionKeyId + } + return "" +} + +func (x *SessionKeyVersion) GetRootKeyVersionId() string { + if x != nil { + return x.RootKeyVersionId + } + return "" +} + +func (x *SessionKeyVersion) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *SessionKeyVersion) GetCtKey() []byte { + if x != nil { + return x.CtKey + } + return nil +} + +func (x *SessionKeyVersion) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *SessionKeyVersion) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +var File_controller_storage_kms_store_v1_session_key_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_session_key_proto_rawDesc = []byte{ + 0x0a, 0x31, 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, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 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, 0x98, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 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, 0x97, 0x02, 0x0a, + 0x11, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 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, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 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_session_key_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_session_key_proto_rawDescData = file_controller_storage_kms_store_v1_session_key_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_session_key_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_session_key_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_session_key_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_session_key_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_session_key_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_session_key_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_kms_store_v1_session_key_proto_goTypes = []interface{}{ + (*SessionKey)(nil), // 0: controller.storage.kms.store.v1.SessionKey + (*SessionKeyVersion)(nil), // 1: controller.storage.kms.store.v1.SessionKeyVersion + (*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_kms_store_v1_session_key_proto_depIdxs = []int32{ + 2, // 0: controller.storage.kms.store.v1.SessionKey.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // 1: controller.storage.kms.store.v1.SessionKeyVersion.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_session_key_proto_init() } +func file_controller_storage_kms_store_v1_session_key_proto_init() { + if File_controller_storage_kms_store_v1_session_key_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_session_key_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SessionKey); 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_session_key_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SessionKeyVersion); 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_session_key_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_session_key_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_session_key_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_session_key_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_session_key_proto = out.File + file_controller_storage_kms_store_v1_session_key_proto_rawDesc = nil + file_controller_storage_kms_store_v1_session_key_proto_goTypes = nil + file_controller_storage_kms_store_v1_session_key_proto_depIdxs = nil +} diff --git a/internal/kms/store/token_key.pb.go b/internal/kms/store/token_key.pb.go new file mode 100644 index 000000000..ed0acf91d --- /dev/null +++ b/internal/kms/store/token_key.pb.go @@ -0,0 +1,334 @@ +// 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/token_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 TokenKey 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 *TokenKey) Reset() { + *x = TokenKey{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_token_key_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TokenKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenKey) ProtoMessage() {} + +func (x *TokenKey) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_token_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 TokenKey.ProtoReflect.Descriptor instead. +func (*TokenKey) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_token_key_proto_rawDescGZIP(), []int{0} +} + +func (x *TokenKey) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *TokenKey) GetRootKeyId() string { + if x != nil { + return x.RootKeyId + } + return "" +} + +func (x *TokenKey) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +type TokenKeyVersion 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"` + // id for the key version + // @inject_tag: `gorm:"default:null"` + TokenKeyId string `protobuf:"bytes,20,opt,name=token_key_id,json=tokenKeyId,proto3" json:"token_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 *TokenKeyVersion) Reset() { + *x = TokenKeyVersion{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_kms_store_v1_token_key_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TokenKeyVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenKeyVersion) ProtoMessage() {} + +func (x *TokenKeyVersion) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_kms_store_v1_token_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 TokenKeyVersion.ProtoReflect.Descriptor instead. +func (*TokenKeyVersion) Descriptor() ([]byte, []int) { + return file_controller_storage_kms_store_v1_token_key_proto_rawDescGZIP(), []int{1} +} + +func (x *TokenKeyVersion) GetPrivateId() string { + if x != nil { + return x.PrivateId + } + return "" +} + +func (x *TokenKeyVersion) GetTokenKeyId() string { + if x != nil { + return x.TokenKeyId + } + return "" +} + +func (x *TokenKeyVersion) GetRootKeyVersionId() string { + if x != nil { + return x.RootKeyVersionId + } + return "" +} + +func (x *TokenKeyVersion) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *TokenKeyVersion) GetCtKey() []byte { + if x != nil { + return x.CtKey + } + return nil +} + +func (x *TokenKeyVersion) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *TokenKeyVersion) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +var File_controller_storage_kms_store_v1_token_key_proto protoreflect.FileDescriptor + +var file_controller_storage_kms_store_v1_token_key_proto_rawDesc = []byte{ + 0x0a, 0x2f, 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, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 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, + 0x96, 0x01, 0x0a, 0x08, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 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, 0x91, 0x02, 0x0a, 0x0f, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 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, 0x20, 0x0a, 0x0c, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 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_token_key_proto_rawDescOnce sync.Once + file_controller_storage_kms_store_v1_token_key_proto_rawDescData = file_controller_storage_kms_store_v1_token_key_proto_rawDesc +) + +func file_controller_storage_kms_store_v1_token_key_proto_rawDescGZIP() []byte { + file_controller_storage_kms_store_v1_token_key_proto_rawDescOnce.Do(func() { + file_controller_storage_kms_store_v1_token_key_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_kms_store_v1_token_key_proto_rawDescData) + }) + return file_controller_storage_kms_store_v1_token_key_proto_rawDescData +} + +var file_controller_storage_kms_store_v1_token_key_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_storage_kms_store_v1_token_key_proto_goTypes = []interface{}{ + (*TokenKey)(nil), // 0: controller.storage.kms.store.v1.TokenKey + (*TokenKeyVersion)(nil), // 1: controller.storage.kms.store.v1.TokenKeyVersion + (*timestamp.Timestamp)(nil), // 2: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_kms_store_v1_token_key_proto_depIdxs = []int32{ + 2, // 0: controller.storage.kms.store.v1.TokenKey.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // 1: controller.storage.kms.store.v1.TokenKeyVersion.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_token_key_proto_init() } +func file_controller_storage_kms_store_v1_token_key_proto_init() { + if File_controller_storage_kms_store_v1_token_key_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_kms_store_v1_token_key_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TokenKey); 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_token_key_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TokenKeyVersion); 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_token_key_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_kms_store_v1_token_key_proto_goTypes, + DependencyIndexes: file_controller_storage_kms_store_v1_token_key_proto_depIdxs, + MessageInfos: file_controller_storage_kms_store_v1_token_key_proto_msgTypes, + }.Build() + File_controller_storage_kms_store_v1_token_key_proto = out.File + file_controller_storage_kms_store_v1_token_key_proto_rawDesc = nil + file_controller_storage_kms_store_v1_token_key_proto_goTypes = nil + file_controller_storage_kms_store_v1_token_key_proto_depIdxs = nil +} diff --git a/internal/kms/testing.go b/internal/kms/testing.go index c44cb9823..94cb58254 100644 --- a/internal/kms/testing.go +++ b/internal/kms/testing.go @@ -94,3 +94,105 @@ func TestDatabaseKeyVersion(t *testing.T, conn *gorm.DB, rootKeyVersionWrapper w require.NoError(err) return k } + +func TestOplogKey(t *testing.T, conn *gorm.DB, rootKeyId string) *OplogKey { + t.Helper() + require := require.New(t) + rw := db.New(conn) + require.NoError(conn.Where("root_key_id = ?", rootKeyId).Delete(AllocOplogKey()).Error) + k, err := NewOplogKey(rootKeyId) + require.NoError(err) + id, err := newOplogKeyId() + require.NoError(err) + k.PrivateId = id + k.RootKeyId = rootKeyId + err = rw.Create(context.Background(), k) + require.NoError(err) + return k +} + +func TestOplogKeyVersion(t *testing.T, conn *gorm.DB, rootKeyVersionWrapper wrapping.Wrapper, oplogKeyId string, key []byte) *OplogKeyVersion { + t.Helper() + require := require.New(t) + rw := db.New(conn) + rootKeyVersionId := rootKeyVersionWrapper.KeyID() + require.NotEmpty(rootKeyVersionId) + k, err := NewOplogKeyVersion(oplogKeyId, key, rootKeyVersionId) + require.NoError(err) + id, err := newOplogKeyVersionId() + 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 +} + +func TestTokenKey(t *testing.T, conn *gorm.DB, rootKeyId string) *TokenKey { + t.Helper() + require := require.New(t) + rw := db.New(conn) + require.NoError(conn.Where("root_key_id = ?", rootKeyId).Delete(AllocTokenKey()).Error) + k, err := NewTokenKey(rootKeyId) + require.NoError(err) + id, err := newTokenKeyId() + require.NoError(err) + k.PrivateId = id + k.RootKeyId = rootKeyId + err = rw.Create(context.Background(), k) + require.NoError(err) + return k +} + +func TestTokenKeyVersion(t *testing.T, conn *gorm.DB, rootKeyVersionWrapper wrapping.Wrapper, tokenKeyId string, key []byte) *TokenKeyVersion { + t.Helper() + require := require.New(t) + rw := db.New(conn) + rootKeyVersionId := rootKeyVersionWrapper.KeyID() + require.NotEmpty(rootKeyVersionId) + k, err := NewTokenKeyVersion(tokenKeyId, key, rootKeyVersionId) + require.NoError(err) + id, err := newTokenKeyVersionId() + 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 +} + +func TestSessionKey(t *testing.T, conn *gorm.DB, rootKeyId string) *SessionKey { + t.Helper() + require := require.New(t) + rw := db.New(conn) + require.NoError(conn.Where("root_key_id = ?", rootKeyId).Delete(AllocSessionKey()).Error) + k, err := NewSessionKey(rootKeyId) + require.NoError(err) + id, err := newSessionKeyId() + require.NoError(err) + k.PrivateId = id + k.RootKeyId = rootKeyId + err = rw.Create(context.Background(), k) + require.NoError(err) + return k +} + +func TestSessionKeyVersion(t *testing.T, conn *gorm.DB, rootKeyVersionWrapper wrapping.Wrapper, sessionKeyId string, key []byte) *SessionKeyVersion { + t.Helper() + require := require.New(t) + rw := db.New(conn) + rootKeyVersionId := rootKeyVersionWrapper.KeyID() + require.NotEmpty(rootKeyVersionId) + k, err := NewSessionKeyVersion(sessionKeyId, key, rootKeyVersionId) + require.NoError(err) + id, err := newSessionKeyVersionId() + 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 6426fb68d..40faecda5 100644 --- a/internal/kms/testing_test.go +++ b/internal/kms/testing_test.go @@ -65,3 +65,96 @@ func Test_TestDatabaseKeyVersion(t *testing.T) { require.NotNil(dv) require.NotEmpty(dv.PrivateId) } + +func Test_TestOplogKey(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) + + opk := kms.TestOplogKey(t, conn, k.PrivateId) + require.NotNil(opk) + assert.NotEmpty(opk.PrivateId) +} + +func Test_TestOplogKeyVersion(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) + opk := kms.TestOplogKey(t, conn, rk.PrivateId) + opv := kms.TestOplogKeyVersion(t, conn, rootKeyVersionWrapper, opk.PrivateId, []byte("test dek key")) + require.NotNil(opv) + require.NotEmpty(opv.PrivateId) +} + +func Test_TestTokenKey(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) + + tk := kms.TestTokenKey(t, conn, k.PrivateId) + require.NotNil(tk) + assert.NotEmpty(tk.PrivateId) +} + +func Test_TestTokenKeyVersion(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) + tk := kms.TestTokenKey(t, conn, rk.PrivateId) + tv := kms.TestTokenKeyVersion(t, conn, rootKeyVersionWrapper, tk.PrivateId, []byte("test dek key")) + require.NotNil(tv) + require.NotEmpty(tv.PrivateId) +} + +func Test_TestSessionKey(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) + + sk := kms.TestSessionKey(t, conn, k.PrivateId) + require.NotNil(sk) + assert.NotEmpty(sk.PrivateId) +} + +func Test_TestSessionKeyVersion(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) + sk := kms.TestSessionKey(t, conn, rk.PrivateId) + sv := kms.TestSessionKeyVersion(t, conn, rootKeyVersionWrapper, sk.PrivateId, []byte("test dek key")) + require.NotNil(sv) + require.NotEmpty(sv.PrivateId) +} diff --git a/internal/kms/token_key.go b/internal/kms/token_key.go new file mode 100644 index 000000000..dcea1aa16 --- /dev/null +++ b/internal/kms/token_key.go @@ -0,0 +1,77 @@ +package kms + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms/store" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultTokenKeyTableName = "kms_token_key" +) + +type TokenKey struct { + *store.TokenKey + tableName string `gorm:"-"` +} + +// NewTokenKey creates a new in memory key. No options +// are currently supported. +func NewTokenKey(rootKeyId string, opt ...Option) (*TokenKey, error) { + if rootKeyId == "" { + return nil, fmt.Errorf("new root key: missing root key id: %w", db.ErrInvalidParameter) + } + c := &TokenKey{ + TokenKey: &store.TokenKey{ + RootKeyId: rootKeyId, + }, + } + return c, nil +} + +// AllocTokenKey will allocate a key +func AllocTokenKey() TokenKey { + return TokenKey{ + TokenKey: &store.TokenKey{}, + } +} + +// Clone creates a clone of the key +func (k *TokenKey) Clone() interface{} { + cp := proto.Clone(k.TokenKey) + return &TokenKey{ + TokenKey: cp.(*store.TokenKey), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the key +// before it's written. +func (k *TokenKey) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("token key vet for write: missing private id: %w", db.ErrInvalidParameter) + } + if opType == db.CreateOp { + if k.RootKeyId == "" { + return fmt.Errorf("token key vet for write: missing root key id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *TokenKey) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultTokenKeyTableName +} + +// 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 *TokenKey) SetTableName(n string) { + k.tableName = n +} diff --git a/internal/kms/token_key_test.go b/internal/kms/token_key_test.go new file mode 100644 index 000000000..afdd901b1 --- /dev/null +++ b/internal/kms/token_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 TokenKey attributes are +// immutable and those tests are covered by TestTokenKey_ImmutableFields + +func TestTokenKey_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.TokenKey + 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.TokenKey { + k := kms.AllocTokenKey() + 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.AllocTokenKey()).Error) + got, err := kms.NewTokenKey(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.TokenKeyPrefix) + 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 TestTokenKey_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.AllocTokenKey()).Error) + + tests := []struct { + name string + key *kms.TokenKey + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestTokenKey(t, conn, rk.PrivateId), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.TokenKey { + k := kms.AllocTokenKey() + id, err := db.NewPublicId(kms.TokenKeyPrefix) + 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.AllocTokenKey() + 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.AllocTokenKey() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestTokenKey_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.TestTokenKey(t, conn, rk.PrivateId) + cp := k.Clone() + assert.True(proto.Equal(cp.(*kms.TokenKey).TokenKey, k.TokenKey)) + }) + 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.TestTokenKey(t, conn, rk.PrivateId) + k2 := kms.TestTokenKey(t, conn, rk2.PrivateId) + + cp := k.Clone() + assert.True(!proto.Equal(cp.(*kms.TokenKey).TokenKey, k2.TokenKey)) + }) +} + +func TestTokenKey_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultTokenKeyTableName + 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.AllocTokenKey() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocTokenKey() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/kms/token_key_version.go b/internal/kms/token_key_version.go new file mode 100644 index 000000000..748546636 --- /dev/null +++ b/internal/kms/token_key_version.go @@ -0,0 +1,114 @@ +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 ( + DefaultTokenKeyVersionTableName = "kms_token_key_version" +) + +type TokenKeyVersion struct { + *store.TokenKeyVersion + tableName string `gorm:"-"` +} + +// TokenKeyVersion creates a new in memory key version. No options are +// currently supported. +func NewTokenKeyVersion(tokenKeyId string, key []byte, rootKeyVersionId string, opt ...Option) (*TokenKeyVersion, error) { + if tokenKeyId == "" { + return nil, fmt.Errorf("new token key version: missing token key id: %w", db.ErrInvalidParameter) + } + if len(key) == 0 { + return nil, fmt.Errorf("new token key version: missing key: %w", db.ErrInvalidParameter) + } + if rootKeyVersionId == "" { + return nil, fmt.Errorf("new token key version: missing root key version id: %w", db.ErrInvalidParameter) + } + + k := &TokenKeyVersion{ + TokenKeyVersion: &store.TokenKeyVersion{ + TokenKeyId: tokenKeyId, + RootKeyVersionId: rootKeyVersionId, + Key: key, + }, + } + return k, nil +} + +// AllocTokenKeyVersion allocates a key version +func AllocTokenKeyVersion() TokenKeyVersion { + return TokenKeyVersion{ + TokenKeyVersion: &store.TokenKeyVersion{}, + } +} + +// Clone creates a clone of the key version +func (k *TokenKeyVersion) Clone() interface{} { + cp := proto.Clone(k.TokenKeyVersion) + return &TokenKeyVersion{ + TokenKeyVersion: cp.(*store.TokenKeyVersion), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the key +// version before it's written. +func (k *TokenKeyVersion) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if k.PrivateId == "" { + return fmt.Errorf("token key version vet for write: missing private id: %w", db.ErrInvalidParameter) + } + if opType == db.CreateOp { + if k.CtKey == nil { + return fmt.Errorf("token key version vet for write: missing key: %w", db.ErrInvalidParameter) + } + if k.TokenKeyId == "" { + return fmt.Errorf("token key version vet for write: missing token key id: %w", db.ErrInvalidParameter) + } + if k.RootKeyVersionId == "" { + return fmt.Errorf("token key version vet for write: missing root key version id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (k *TokenKeyVersion) TableName() string { + if k.tableName != "" { + return k.tableName + } + return DefaultTokenKeyVersionTableName +} + +// 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 *TokenKeyVersion) SetTableName(n string) { + k.tableName = n +} + +// Encrypt will encrypt the key version's key +func (k *TokenKeyVersion) Encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.TokenKeyVersion directly + if err := structwrapping.WrapStruct(ctx, cipher, k.TokenKeyVersion, nil); err != nil { + return fmt.Errorf("error encrypting kms token key version: %w", err) + } + return nil +} + +// Decrypt will decrypt the key version's key +func (k *TokenKeyVersion) Decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + // structwrapping doesn't support embedding, so we'll pass in the + // store.TokenKeyVersion directly + if err := structwrapping.UnwrapStruct(ctx, cipher, k.TokenKeyVersion, nil); err != nil { + return fmt.Errorf("error decrypting kms token key version: %w", err) + } + return nil +} diff --git a/internal/kms/token_key_version_test.go b/internal/kms/token_key_version_test.go new file mode 100644 index 000000000..33af96de6 --- /dev/null +++ b/internal/kms/token_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 TokenKeyVersion attributes are +// immutable and those tests are covered by TestTokenKey_ImmutableFields + +func TestTokenKeyVersion_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.TestTokenKey(t, conn, rk.PrivateId) + + type args struct { + tokenKeyId string + key []byte + rootKeyVersionId string + opt []kms.Option + } + tests := []struct { + name string + args args + want *kms.TokenKeyVersion + wantErr bool + wantIsErr error + create bool + wantCreateErr bool + }{ + { + name: "empty-id", + args: args{ + key: []byte("test key"), + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-key", + args: args{ + tokenKeyId: dk.PrivateId, + rootKeyVersionId: rkv.PrivateId, + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-root-key-version-id", + args: args{ + tokenKeyId: dk.PrivateId, + key: []byte("test key"), + }, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "valid", + args: args{ + tokenKeyId: dk.PrivateId, + key: []byte("test key"), + rootKeyVersionId: rkv.PrivateId, + }, + want: func() *kms.TokenKeyVersion { + k := kms.AllocTokenKeyVersion() + k.RootKeyVersionId = rkv.PrivateId + k.Key = []byte("test key") + k.TokenKeyId = 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.AllocTokenKeyVersion()).Error) + got, err := kms.NewTokenKeyVersion(tt.args.tokenKeyId, 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.TokenKeyPrefix) + 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 TestTokenKeyVersion_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) + tk := kms.TestTokenKey(t, conn, rk.PrivateId) + + require.NoError(t, conn.Where("1=1").Delete(kms.AllocTokenKeyVersion()).Error) + + tests := []struct { + name string + key *kms.TokenKeyVersion + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + key: kms.TestTokenKeyVersion(t, conn, rkvWrapper, tk.PrivateId, []byte("test key")), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + key: func() *kms.TokenKeyVersion { + k := kms.AllocTokenKeyVersion() + id, err := db.NewPublicId(kms.TokenKeyVersionPrefix) + 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.AllocTokenKeyVersion() + 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.AllocTokenKeyVersion() + foundKey.PrivateId = tt.key.PrivateId + err = rw.LookupById(context.Background(), &foundKey) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestTokenKeyVersion_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.TestTokenKey(t, conn, rk.PrivateId) + kv := kms.TestTokenKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test key")) + cp := kv.Clone() + assert.True(proto.Equal(cp.(*kms.TokenKeyVersion).TokenKeyVersion, kv.TokenKeyVersion)) + }) + 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.TestTokenKey(t, conn, rk.PrivateId) + k2 := kms.TestTokenKey(t, conn, rk2.PrivateId) + kv := kms.TestTokenKeyVersion(t, conn, rkvWrapper, k.PrivateId, []byte("test key")) + kv2 := kms.TestTokenKeyVersion(t, conn, rkvWrapper2, k2.PrivateId, []byte("test key 2")) + + cp := kv.Clone() + assert.True(!proto.Equal(cp.(*kms.TokenKeyVersion).TokenKeyVersion, kv2.TokenKeyVersion)) + }) +} + +func TestTokenKeyVersion_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := kms.DefaultTokenKeyVersionTableName + 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.AllocTokenKeyVersion() + require.Equal(defaultTableName, def.TableName()) + s := kms.AllocTokenKeyVersion() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/proto/local/controller/storage/kms/store/v1/oplog_key.proto b/internal/proto/local/controller/storage/kms/store/v1/oplog_key.proto new file mode 100644 index 000000000..26419b88c --- /dev/null +++ b/internal/proto/local/controller/storage/kms/store/v1/oplog_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 OplogKey { + // 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 OplogKeyVersion { + // private_id is used to access the key version via an API + // @inject_tag: gorm:"primary_key" + string private_id = 10; + + // id for the key version + // @inject_tag: `gorm:"default:null"` + string oplog_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 diff --git a/internal/proto/local/controller/storage/kms/store/v1/session_key.proto b/internal/proto/local/controller/storage/kms/store/v1/session_key.proto new file mode 100644 index 000000000..9384f739c --- /dev/null +++ b/internal/proto/local/controller/storage/kms/store/v1/session_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 SessionKey { + // 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 SessionKeyVersion { + // private_id is used to access the key version via an API + // @inject_tag: gorm:"primary_key" + string private_id = 10; + + // id for the key version + // @inject_tag: `gorm:"default:null"` + string session_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 diff --git a/internal/proto/local/controller/storage/kms/store/v1/token_key.proto b/internal/proto/local/controller/storage/kms/store/v1/token_key.proto new file mode 100644 index 000000000..27deba91f --- /dev/null +++ b/internal/proto/local/controller/storage/kms/store/v1/token_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 TokenKey { + // 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 TokenKeyVersion { + // private_id is used to access the key version via an API + // @inject_tag: gorm:"primary_key" + string private_id = 10; + + // id for the key version + // @inject_tag: `gorm:"default:null"` + string token_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