package kms import ( "context" "fmt" "io" "reflect" "sync" "time" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/go-dbw" wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" wrapping "github.com/hashicorp/go-kms-wrapping/v2" ) // Kms is a way to access wrappers for a given scope and purpose. Since keys can // never change, only be added or (eventually) removed, it opportunistically // caches, going to the database as needed. type Kms struct { underlying *wrappingKms.Kms reader db.Reader derivedPurposeCache sync.Map } // New creates a Kms using the provided reader and writer. No options are // currently supported. func New(ctx context.Context, reader *db.Db, writer *db.Db, _ ...Option) (*Kms, error) { const op = "kms.(Kms).New" if isNil(reader) { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing reader") } if isNil(writer) { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing writer") } purposes := stdNewKmsPurposes() r := dbw.New(reader.UnderlyingDB()) w := dbw.New(writer.UnderlyingDB()) k, err := wrappingKms.New(r, w, purposes, wrappingKms.WithCache(true)) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating new in-memory kms")) } return &Kms{ underlying: k, reader: reader, }, nil } // NewUsingReaderWriter creates a Kms using the provided reader and writer. No // options are currently supported. func NewUsingReaderWriter(ctx context.Context, reader db.Reader, writer db.Writer, _ ...Option) (*Kms, error) { const op = "kms.(Kms).New" if isNil(reader) { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing reader") } if isNil(writer) { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing writer") } purposes := stdNewKmsPurposes() r, err := convertToRW(ctx, reader) if err != nil { return nil, errors.Wrap(ctx, err, op) } w, err := convertToRW(ctx, writer) if err != nil { return nil, errors.Wrap(ctx, err, op) } k, err := wrappingKms.New(r, w, purposes, wrappingKms.WithCache(true)) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error creating new in-memory kms")) } return &Kms{ underlying: k, reader: reader, }, nil } func convertToRW(ctx context.Context, r interface{}) (*dbw.RW, error) { const op = "kms.convertToDb" d, ok := r.(*db.Db) if !ok { return nil, errors.New(ctx, errors.InvalidParameter, op, "unable to convert to db.DB") } return dbw.New(d.UnderlyingDB()), nil } // AddExternalWrappers allows setting the external keys. func (k *Kms) AddExternalWrappers(ctx context.Context, opt ...Option) error { const op = "kms.(Kms).AddExternalWrappers" opts := getOpts(opt...) if opts.withRootWrapper != nil { if err := k.underlying.AddExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeRootKey.String()), opts.withRootWrapper); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add root wrapper")) } } if opts.withWorkerAuthWrapper != nil { if err := k.underlying.AddExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), opts.withWorkerAuthWrapper); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add worker auth wrapper")) } } if opts.withRecoveryWrapper != nil { if err := k.underlying.AddExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeRecovery.String()), opts.withRecoveryWrapper); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add recovery wrapper")) } } return nil } // GetWrapper returns a wrapper for the given scope and purpose. When a keyId is // passed, it will ensure that the returning wrapper has that key ID in the // multiwrapper. This is not necessary for encryption but should be supplied for // decryption. func (k *Kms) GetWrapper(ctx context.Context, scopeId string, purpose KeyPurpose, opt ...Option) (wrapping.Wrapper, error) { const op = "kms.(Kms).GetWrapper" if scopeId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") } if purpose == KeyPurposeUnknown { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing purpose") } opts := getOpts(opt...) w, err := k.underlying.GetWrapper(ctx, scopeId, wrappingKms.KeyPurpose(purpose.String()), wrappingKms.WithKeyId(opts.withKeyId)) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get wrapper")) } return w, err } // GetExternalWrappers returns the Kms' ExternalWrappers func (k *Kms) GetExternalWrappers(ctx context.Context) *ExternalWrappers { const op = "kms.(Kms).GetExternalWrappers" ret := &ExternalWrappers{} if root, err := k.underlying.GetExternalRootWrapper(); err == nil { ret.root = root } if workerAuth, err := k.underlying.GetExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String())); err == nil { ret.workerAuth = workerAuth } if recovery, err := k.underlying.GetExternalWrapper(ctx, wrappingKms.KeyPurpose(KeyPurposeRecovery.String())); err == nil { ret.recovery = recovery } return ret } // GetDerivedPurposeCache returns the raw derived purpose cache func (k *Kms) GetDerivedPurposeCache() *sync.Map { return &k.derivedPurposeCache } // ReconcileKeys will reconcile the keys in the kms against known possible // issues. This function reconciles the global scope unless the // WithScopeIds(...) option is provided func (k *Kms) ReconcileKeys(ctx context.Context, randomReader io.Reader, opt ...Option) error { const op = "kms.ReconcileKeys" if isNil(randomReader) { return errors.New(ctx, errors.InvalidParameter, op, "missing rand reader") } // it's possible that the global audit key was created after this instance's // database was initialized... so check if the audit wrapper is available // for the global scope and if not, then add one to the global scope if _, err := k.GetWrapper(ctx, scope.Global.String(), KeyPurposeAudit); err != nil { if err := k.underlying.ReconcileKeys( ctx, []string{scope.Global.String()}, []wrappingKms.KeyPurpose{wrappingKms.KeyPurpose(KeyPurposeAudit.String())}, wrappingKms.WithRandomReader(randomReader), ); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error creating audit key in scope %s", scope.Global.String()))) } } opts := getOpts(opt...) if len(opts.withScopeIds) > 0 { if err := k.underlying.ReconcileKeys( ctx, opts.withScopeIds, // just add additional purposes as needed going forward to reconcile // new keys as they are added. []wrappingKms.KeyPurpose{wrappingKms.KeyPurpose(KeyPurposeOidc.String())}, wrappingKms.WithRandomReader(randomReader), ); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error creating keys in scopes %s", opts.withScopeIds))) } } return nil } // CreateKeys creates the root key and DEKs returns a map of the new keys. // Supports the WithRandomReader(...) and WithReaderWriter(...) options. // When WithReaderWriter(...) is used the caller is responsible for managing the // transaction which allows this capability to be shared with the iam repo when // it's creating Scopes. func (k *Kms) CreateKeys(ctx context.Context, scopeId string, opt ...Option) error { const op = "kms.(Kms).CreateKeysTx" if scopeId == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") } opts := getOpts(opt...) kmsOpts := []wrappingKms.Option{wrappingKms.WithRandomReader(opts.withRandomReader)} switch { case !isNil(opts.withReader) && isNil(opts.withWriter): return errors.New(ctx, errors.InvalidParameter, op, "missing writer") case isNil(opts.withReader) && !isNil(opts.withWriter): return errors.New(ctx, errors.InvalidParameter, op, "missing reader") case !isNil(opts.withReader) && !isNil(opts.withWriter): r, err := convertToRW(ctx, opts.withReader) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert reader")) } w, err := convertToRW(ctx, opts.withWriter) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert writer")) } kmsOpts = append(kmsOpts, wrappingKms.WithReaderWriter(r, w)) } purposes := make([]wrappingKms.KeyPurpose, 0, len(ValidDekPurposes())) for _, p := range ValidDekPurposes() { purposes = append(purposes, wrappingKms.KeyPurpose(p.String())) } if err := k.underlying.CreateKeys(ctx, scopeId, purposes, kmsOpts...); err != nil { return errors.Wrap(ctx, err, op) } return nil } // ClearCache will clear the kms's cache which is useful after a scope has been // deleted. func (k *Kms) ClearCache(ctx context.Context) error { const op = "kms.(Kms).ClearCache" if err := k.underlying.ClearCache(ctx); err != nil { return errors.Wrap(ctx, err, op) } return nil } // VerifyGlobalRoot will verify that the global root wrapper is reasonable. func (k *Kms) VerifyGlobalRoot(ctx context.Context) error { const op = "kms.(Kms).VerifyGlobalRoot" var keys []*rootKey if err := k.reader.SearchWhere(ctx, &keys, "1=1", nil, db.WithLimit(1), db.WithOrder("create_time asc")); err != nil { return errors.Wrap(ctx, err, op) } for _, rk := range keys { if rk.ScopeId == scope.Global.String() { return nil } } return errors.New(ctx, errors.MigrationIntegrity, op, "can't find global scoped root key") } func stdNewKmsPurposes() []wrappingKms.KeyPurpose { purposes := make([]wrappingKms.KeyPurpose, 0, len(ValidDekPurposes())) for _, p := range ValidDekPurposes() { purposes = append(purposes, wrappingKms.KeyPurpose(p.String())) } purposes = append(purposes, wrappingKms.KeyPurpose(KeyPurposeWorkerAuth.String()), wrappingKms.KeyPurpose(KeyPurposeWorkerAuthStorage.String()), wrappingKms.KeyPurpose(KeyPurposeRecovery.String())) return purposes } func isNil(i interface{}) bool { if i == nil { return true } switch reflect.TypeOf(i).Kind() { case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: return reflect.ValueOf(i).IsNil() } return false } type rootKey struct { PrivateId string `gorm:"primary_key"` ScopeId string `gorm:"default:null"` CreateTime time.Time `gorm:"default:current_timestamp"` } func (*rootKey) TableName() string { return "kms_root_key" }