From 67f9df979fc7b5094850b6e5be057b9cd6dd99fd Mon Sep 17 00:00:00 2001
From: Luiz <luizgn@gmail.com>
Date: Tue, 25 Aug 2020 10:59:33 -0300
Subject: [PATCH] Add IBM Key Protect wrapper

---
 README.md                        |   1 +
 go.mod                           |   1 +
 go.sum                           |   6 +
 wrapper.go                       |   1 +
 wrappers/ibmkp/ibmkp.go          | 249 +++++++++++++++++++++++++++++++
 wrappers/ibmkp/ibmkp_acc_test.go | 110 ++++++++++++++
 6 files changed, 368 insertions(+)
 create mode 100644 wrappers/ibmkp/ibmkp.go
 create mode 100644 wrappers/ibmkp/ibmkp_acc_test.go

diff --git a/README.md b/README.md
index dd90ed88..55aab828 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ as they may have been used for past encryption operations.
   * * Azure KeyVault (uses envelopes)
   * * GCP CKMS (uses envelopes)
   * * Huawei Cloud KMS (uses envelopes)
+  * * IBM Key Protect (uses envelopes)
   * * OCI KMS (uses envelopes)
   * * Tencent Cloud KMS (uses envelopes)
   * * Vault Transit mount
diff --git a/go.mod b/go.mod
index 8808cb35..e9068df5 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/Azure/go-autorest/autorest/azure/auth v0.4.2
 	github.com/Azure/go-autorest/autorest/to v0.3.0
 	github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
+	github.com/IBM/keyprotect-go-client v0.5.0
 	github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f
 	github.com/aws/aws-sdk-go v1.30.27
 	github.com/golang/protobuf v1.4.2
diff --git a/go.sum b/go.sum
index bb90dc60..7469e73b 100644
--- a/go.sum
+++ b/go.sum
@@ -59,6 +59,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/IBM/keyprotect-go-client v0.5.0 h1:kTvP7BC723b8fVXnaNTUXOKBsYRechK24dNDxvPfCEU=
+github.com/IBM/keyprotect-go-client v0.5.0/go.mod h1:5TwDM/4FRJq1ZOlwQL1xFahLWQ3TveR88VmL1u3njyI=
 github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
 github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -197,6 +199,7 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
@@ -204,6 +207,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@@ -324,6 +328,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
 github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -665,6 +670,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
 gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
diff --git a/wrapper.go b/wrapper.go
index 64ee34c4..d60a6a25 100644
--- a/wrapper.go
+++ b/wrapper.go
@@ -14,6 +14,7 @@ const (
 	AzureKeyVault   = "azurekeyvault"
 	GCPCKMS         = "gcpckms"
 	HuaweiCloudKMS  = "huaweicloudkms"
+	IBMKP           = "ibmkp"
 	MultiWrapper    = "multiwrapper"
 	OCIKMS          = "ocikms"
 	PKCS11          = "pkcs11"
diff --git a/wrappers/ibmkp/ibmkp.go b/wrappers/ibmkp/ibmkp.go
new file mode 100644
index 00000000..bddf876f
--- /dev/null
+++ b/wrappers/ibmkp/ibmkp.go
@@ -0,0 +1,249 @@
+package ibmkp
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"os"
+	"sync/atomic"
+
+	kp "github.com/IBM/keyprotect-go-client"
+	wrapping "github.com/hashicorp/go-kms-wrapping"
+)
+
+// These constants contain the accepted env vars
+const (
+	EnvIBMApiKey       = "IBMCLOUD_API_KEY"
+	EnvIBMKPEndpoint   = "IBMCLOUD_KP_ENDPOINT"
+	EnvIBMKPInstanceID = "IBMCLOUD_KP_INSTANCE_ID"
+	EnvIBMKPKeyID      = "IBMCLOUD_KP_KEY_ID"
+)
+
+// Wrapper represents credentials and Key information for the KMS Key used to
+// encryption and decryption
+type Wrapper struct {
+	endpoint   string
+	apiKey     string
+	instanceID string
+	keyID      string
+
+	currentKeyID *atomic.Value
+
+	client *kp.Client
+}
+
+// Ensure that we are implementing Wrapper
+var _ wrapping.Wrapper = (*Wrapper)(nil)
+
+// NewWrapper creates a new IBMKP wrapper with the provided options
+func NewWrapper(opts *wrapping.WrapperOptions) *Wrapper {
+	if opts == nil {
+		opts = new(wrapping.WrapperOptions)
+	}
+	k := &Wrapper{
+		currentKeyID: new(atomic.Value),
+	}
+	k.currentKeyID.Store("")
+	return k
+}
+
+// SetConfig sets the fields on the Wrapper object based on
+// values from the config parameter.
+//
+// Order of precedence IBM Key Protect values:
+// * Environment variable
+// * Value from Vault configuration file
+func (k *Wrapper) SetConfig(config map[string]string) (map[string]string, error) {
+	if config == nil {
+		config = map[string]string{}
+	}
+
+	// Check and set API Key
+	switch {
+	case os.Getenv(EnvIBMApiKey) != "":
+		k.apiKey = os.Getenv(EnvIBMApiKey)
+	case config["api_key"] != "":
+		k.apiKey = config["api_key"]
+	default:
+		return nil, fmt.Errorf("'api_key' was not found for IBM Key Protect wrapper configuration")
+	}
+
+	// Check and set Endpoint
+	switch {
+	case os.Getenv(EnvIBMKPEndpoint) != "":
+		k.endpoint = os.Getenv(EnvIBMKPEndpoint)
+	case config["endpoint"] != "":
+		k.endpoint = config["endpoint"]
+	default:
+		k.endpoint = kp.DefaultBaseURL
+	}
+
+	// Check and set instanceID
+	switch {
+	case os.Getenv(EnvIBMKPInstanceID) != "":
+		k.instanceID = os.Getenv(EnvIBMKPInstanceID)
+	case config["instance_id"] != "":
+		k.instanceID = config["instance_id"]
+	default:
+		return nil, fmt.Errorf("'instance_id' was not found for IBM Key Protect wrapper configuration")
+	}
+
+	// Check and set keyID
+	switch {
+	case os.Getenv(EnvIBMKPKeyID) != "":
+		k.keyID = os.Getenv(EnvIBMKPKeyID)
+	case config["key_id"] != "":
+		k.keyID = config["key_id"]
+	default:
+		return nil, fmt.Errorf("'key_id' was not found for IBM Key Protect wrapper configuration")
+	}
+
+	// Check and set k.client
+	if k.client == nil {
+		client, err := k.GetIBMKPClient()
+		if err != nil {
+			return nil, fmt.Errorf("error initializing IBM Key Protect wrapping client: %w", err)
+		}
+
+		// Test the client connection using provided key ID
+		key, err := client.GetKeyMetadata(context.Background(), k.keyID)
+		if err != nil {
+			return nil, fmt.Errorf("error fetching IBM Key Protect wrapping key information: %w", err)
+		}
+		if key == nil || key.ID == "" {
+			return nil, errors.New("no key information returned")
+		}
+		k.currentKeyID.Store(key.ID)
+
+		k.client = client
+	}
+
+	// Map that holds non-sensitive configuration info
+	wrappingInfo := make(map[string]string)
+	wrappingInfo["endpoint"] = k.endpoint
+	wrappingInfo["instance_id"] = k.instanceID
+	wrappingInfo["key_id"] = k.keyID
+
+	return wrappingInfo, nil
+}
+
+// Init is called during core.Initialize. No-op at the moment.
+func (k *Wrapper) Init(_ context.Context) error {
+	return nil
+}
+
+// Finalize is called during shutdown. This is a no-op since
+// Wrapper doesn't require any cleanup.
+func (k *Wrapper) Finalize(_ context.Context) error {
+	return nil
+}
+
+// Type returns the wrapping type for this particular Wrapper implementation
+func (k *Wrapper) Type() string {
+	return wrapping.IBMKP
+}
+
+// KeyID returns the last known key id
+func (k *Wrapper) KeyID() string {
+	return k.currentKeyID.Load().(string)
+}
+
+// HMACKeyID returns the last known HMAC key id
+func (k *Wrapper) HMACKeyID() string {
+	return ""
+}
+
+// Encrypt is used to encrypt the master key using the the AWS CMK.
+// This returns the ciphertext, and/or any errors from this
+// call. This should be called after the KMS client has been instantiated.
+func (k *Wrapper) Encrypt(ctx context.Context, plaintext, aad []byte) (blob *wrapping.EncryptedBlobInfo, err error) {
+	if plaintext == nil {
+		return nil, fmt.Errorf("given plaintext for encryption is nil")
+	}
+
+	env, err := wrapping.NewEnvelope(nil).Encrypt(plaintext, aad)
+	if err != nil {
+		return nil, fmt.Errorf("error wrapping data: %w", err)
+	}
+
+	if k.client == nil {
+		return nil, fmt.Errorf("nil client")
+	}
+
+	envelopKeyBase64 := []byte(base64.StdEncoding.EncodeToString(env.Key))
+	ciphertext, err := k.client.Wrap(ctx, k.keyID, envelopKeyBase64, nil)
+	if err != nil {
+		return nil, fmt.Errorf("error encrypting data: %w", err)
+	}
+
+	k.currentKeyID.Store(k.keyID)
+
+	ret := &wrapping.EncryptedBlobInfo{
+		Ciphertext: env.Ciphertext,
+		IV:         env.IV,
+		KeyInfo: &wrapping.KeyInfo{
+			KeyID:      k.keyID,
+			WrappedKey: ciphertext,
+		},
+	}
+
+	return ret, nil
+}
+
+// Decrypt is used to decrypt the ciphertext. This should be called after Init.
+func (k *Wrapper) Decrypt(ctx context.Context, in *wrapping.EncryptedBlobInfo, aad []byte) (pt []byte, err error) {
+	if in == nil {
+		return nil, errors.New("given input for decryption is nil")
+	}
+
+	if in.KeyInfo == nil {
+		return nil, errors.New("key info is nil")
+	}
+
+	envelopKeyBase64, err := k.client.Unwrap(ctx, in.KeyInfo.KeyID, in.KeyInfo.WrappedKey, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	envelopKey, err := base64.StdEncoding.DecodeString(string(envelopKeyBase64))
+	if err != nil {
+		return nil, err
+	}
+
+	envInfo := &wrapping.EnvelopeInfo{
+		Key:        envelopKey,
+		IV:         in.IV,
+		Ciphertext: in.Ciphertext,
+	}
+
+	plaintext, err := wrapping.NewEnvelope(nil).Decrypt(envInfo, aad)
+	if err != nil {
+		return nil, fmt.Errorf("error decrypting data with envelope: %w", err)
+	}
+
+	return plaintext, nil
+}
+
+func (k *Wrapper) getConfigAPIKey() kp.ClientConfig {
+	return kp.ClientConfig{
+		BaseURL:    k.endpoint,
+		APIKey:     k.apiKey,
+		TokenURL:   kp.DefaultTokenURL,
+		InstanceID: k.instanceID,
+		Verbose:    kp.VerboseFailOnly,
+	}
+}
+
+// GetIBMKPClient returns an instance of the KMS client.
+func (k *Wrapper) GetIBMKPClient() (*kp.Client, error) {
+
+	options := k.getConfigAPIKey()
+	api, err := kp.New(options, kp.DefaultTransport())
+	if err != nil {
+		return nil, err
+	}
+
+	return api, nil
+
+}
diff --git a/wrappers/ibmkp/ibmkp_acc_test.go b/wrappers/ibmkp/ibmkp_acc_test.go
new file mode 100644
index 00000000..78197187
--- /dev/null
+++ b/wrappers/ibmkp/ibmkp_acc_test.go
@@ -0,0 +1,110 @@
+package ibmkp
+
+// These tests execute real calls. They require:
+// 1. IBM Cloud account
+//    https://cloud.ibm.com/docs/overview?topic=overview-quickstart_lite
+//
+// 2. IBM Key Protect instance
+//    https://cloud.ibm.com/docs/key-protect?topic=key-protect-provision
+//
+// 3. IBM Key Protect's Root Key
+//    https://cloud.ibm.com/docs/key-protect?topic=key-protect-create-root-keys
+//
+// 4. IBM Cloud Service ID with an API Key
+//    https://cloud.ibm.com/docs/account?topic=account-serviceids
+//
+// 5. Grant 'Reader' access to Service ID (step 4) into Root Key (step 3)
+//    https://cloud.ibm.com/docs/key-protect?topic=key-protect-grant-access-keys
+//
+// No costs are involved to setup IBM Cloud environment because IBM Cloud account
+// can be created for free and IBM Key Protect allows up to 20 keys for free.
+//
+// To run this test, the following env variables need to be set:
+//   - IBMCLOUD_API_KEY created on step 4
+//   - IBMCLOUD_KP_INSTANCE_ID created on step 2
+//   - IBMCLOUD_KP_KEY_ID created on step 3
+
+import (
+	"context"
+	"os"
+	"reflect"
+	"testing"
+)
+
+const (
+	TestIBMApiKey       = "notARealApiKey"
+	TestIBMKPInstanceID = "a6493c3a-5b29-4ac3-9eaa-deadbeef3bfd"
+	TestIBMKPKeyID      = "1234abcd-abcd-asdf-3dea-beefdeadabcd"
+)
+
+func TestIBMKP_SetConfig(t *testing.T) {
+
+	checkAndSetEnvVars(t)
+
+	s := NewWrapper(nil)
+	instanceID := os.Getenv(EnvIBMKPInstanceID)
+	os.Unsetenv(EnvIBMKPInstanceID)
+
+	// Attempt to set config, expect failure due to missing config
+	_, err := s.SetConfig(nil)
+	if err == nil {
+		t.Fatal("expected error when IBM Key Protect Key Vault config values are not provided")
+	}
+
+	os.Setenv(EnvIBMKPInstanceID, instanceID)
+
+	_, err = s.SetConfig(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestIBMKP_Lifecycle(t *testing.T) {
+
+	checkAndSetEnvVars(t)
+
+	s := NewWrapper(nil)
+	_, err := s.SetConfig(nil)
+	if err != nil {
+		t.Fatalf("err: %s", err.Error())
+	}
+
+	// Test Encrypt and Decrypt calls
+	input := []byte("foo")
+	swi, err := s.Encrypt(context.Background(), input, nil)
+	if err != nil {
+		t.Fatalf("error encrypting: %s", err.Error())
+	}
+
+	pt, err := s.Decrypt(context.Background(), swi, nil)
+	if err != nil {
+		t.Fatalf("error decrypting: %s", err.Error())
+	}
+
+	if !reflect.DeepEqual(input, pt) {
+		t.Fatalf("expected %s, got %s", input, pt)
+	}
+}
+
+// checkAndSetEnvVars check and sets the required env vars. It will skip tests that are
+// not ran as acceptance tests since they require calling to external APIs.
+func checkAndSetEnvVars(t *testing.T) {
+	t.Helper()
+
+	// Skip tests if we are not running acceptance tests
+	if os.Getenv("VAULT_ACC") == "" {
+		t.SkipNow()
+	}
+
+	if os.Getenv(EnvIBMApiKey) == "" {
+		os.Setenv(EnvIBMApiKey, TestIBMApiKey)
+	}
+
+	if os.Getenv(EnvIBMKPInstanceID) == "" {
+		os.Setenv(EnvIBMKPInstanceID, TestIBMKPInstanceID)
+	}
+
+	if os.Getenv(EnvIBMKPKeyID) == "" {
+		os.Setenv(EnvIBMKPKeyID, TestIBMKPKeyID)
+	}
+}