diff --git a/README.md b/README.md index caddfa38..59c4e5cb 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,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/const.go b/const.go index f9d75a0b..87801145 100644 --- a/const.go +++ b/const.go @@ -15,6 +15,7 @@ const ( WrapperTypeGcpCkms WrapperType = "gcpckms" WrapperTypeHsmAuto WrapperType = "hsm-auto" WrapperTypeHuaweiCloudKms WrapperType = "huaweicloudkms" + WrapperTypeIbmKp WrapperType = "ibmkeyprotect" WrapperTypeOciKms WrapperType = "ocikms" WrapperTypePkcs11 WrapperType = "pkcs11" WrapperTypePooled WrapperType = "pooled" diff --git a/wrappers/ibmkp/go.mod b/wrappers/ibmkp/go.mod new file mode 100644 index 00000000..113361df --- /dev/null +++ b/wrappers/ibmkp/go.mod @@ -0,0 +1,34 @@ +module github.com/hashicorp/go-kms-wrapping/wrappers/ibmkp/v2 + +go 1.23.0 + +replace github.com/hashicorp/go-kms-wrapping/v2 => ../.. + +require ( + github.com/IBM/keyprotect-go-client v0.15.1 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-kms-wrapping/v2 v2.0.18 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.6 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.8.1 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + golang.org/x/sys v0.33.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/wrappers/ibmkp/go.sum b/wrappers/ibmkp/go.sum new file mode 100644 index 00000000..2b7d18ea --- /dev/null +++ b/wrappers/ibmkp/go.sum @@ -0,0 +1,91 @@ +github.com/IBM/keyprotect-go-client v0.15.1 h1:m4qzqF5zOumRxKZ8s7vtK7A/UV/D278L8xpRG+WgT0s= +github.com/IBM/keyprotect-go-client v0.15.1/go.mod h1:asXtHwL/4uCHA221Vd/7SkXEi2pcRHDzPyyksc1DthE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 h1:FW0YttEnUNDJ2WL9XcrrfteS1xW8u+sh4ggM8pN5isQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= +github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/wrappers/ibmkp/ibmkp.go b/wrappers/ibmkp/ibmkp.go new file mode 100644 index 00000000..c11aeb00 --- /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" + "github.com/hashicorp/go-hclog" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" +) + +// 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 { + apiKey string + endpoint string + instanceId string + keyId string + + keyNotRequired bool + + currentkeyId *atomic.Value + + client *kp.Client + + logger hclog.Logger +} + +// Ensure that we are implementing Wrapper +var _ wrapping.Wrapper = (*Wrapper)(nil) + +// NewWrapper creates a new IBMKP wrapper with the provided options +func NewWrapper() *Wrapper { + 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 +// * Passed in config map +func (k *Wrapper) SetConfig(_ context.Context, opt ...wrapping.Option) (*wrapping.WrapperConfig, error) { + opts, err := getOpts(opt...) + if err != nil { + return nil, err + } + + k.keyNotRequired = opts.withKeyNotRequired + k.logger = opts.withLogger + + // Check and set API Key + switch { + case os.Getenv(EnvIbmApiKey) != "" && !opts.withDisallowEnvVars: + k.apiKey = os.Getenv(EnvIbmApiKey) + case opts.withApiKey != "": + k.apiKey = opts.withApiKey + case k.keyNotRequired: + // key not required to set config + default: + return nil, fmt.Errorf("'api_key' was not found in env or config for IBM Key Protect wrapper configuration") + } + + // Check and set Endpoint + switch { + case os.Getenv(EnvIbmKpEndpoint) != "" && !opts.withDisallowEnvVars: + k.endpoint = os.Getenv(EnvIbmKpEndpoint) + case opts.withEndpoint != "": + k.endpoint = opts.withEndpoint + default: + k.endpoint = kp.DefaultBaseURL + } + + // Check and set instanceId + switch { + case os.Getenv(EnvIbmKpInstanceId) != "" && !opts.withDisallowEnvVars: + k.instanceId = os.Getenv(EnvIbmKpInstanceId) + case opts.withInstanceId != "": + k.instanceId = opts.withInstanceId + case k.keyNotRequired: + // key not required to set config + default: + return nil, fmt.Errorf("'instance_id' was not found in env or config for IBM Key Protect wrapper configuration") + } + + // Check and set keyId + switch { + case os.Getenv(EnvIbmKpKeyId) != "" && !opts.withDisallowEnvVars: + k.keyId = os.Getenv(EnvIbmKpKeyId) + case opts.WithKeyId != "": + k.keyId = opts.WithKeyId + default: + return nil, fmt.Errorf("'key_id' was not found in env or config 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) + } + + if !k.keyNotRequired { + // 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 + wrapConfig := new(wrapping.WrapperConfig) + wrapConfig.Metadata = make(map[string]string) + wrapConfig.Metadata["endpoint"] = k.endpoint + wrapConfig.Metadata["instance_id"] = k.instanceId + wrapConfig.Metadata["key_id"] = k.keyId + + return wrapConfig, nil +} + +// Type returns the wrapping type for this particular Wrapper implementation +func (k *Wrapper) Type(_ context.Context) (wrapping.WrapperType, error) { + return wrapping.WrapperTypeIbmKp, nil +} + +// keyId returns the last known key id +func (k *Wrapper) KeyId(_ context.Context) (string, error) { + return k.currentkeyId.Load().(string), nil +} + +// Encrypt is used to encrypt the master key using the IBM KeyProtect API. +// 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 []byte, opt ...wrapping.Option) (*wrapping.BlobInfo, error) { + if plaintext == nil { + return nil, fmt.Errorf("given plaintext for encryption is nil") + } + + env, err := wrapping.EnvelopeEncrypt(plaintext, opt...) + if err != nil { + return nil, fmt.Errorf("error wrapping data: %w", err) + } + + if k.client == nil { + return nil, fmt.Errorf("nil client") + } + + envelopeKeyBase64 := []byte(base64.StdEncoding.EncodeToString(env.Key)) + ciphertext, err := k.client.Wrap(ctx, k.keyId, envelopeKeyBase64, nil) + if err != nil { + return nil, fmt.Errorf("error encrypting data: %w", err) + } + + k.currentkeyId.Store(k.keyId) + + ret := &wrapping.BlobInfo{ + 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.BlobInfo, opt ...wrapping.Option) ([]byte, 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") + } + + envelopeKeyBase64, err := k.client.Unwrap(ctx, in.KeyInfo.KeyId, in.KeyInfo.WrappedKey, nil) + if err != nil { + return nil, err + } + + envelopeKey, err := base64.StdEncoding.DecodeString(string(envelopeKeyBase64)) + if err != nil { + return nil, err + } + + envInfo := &wrapping.EnvelopeInfo{ + Key: envelopeKey, + Iv: in.Iv, + Ciphertext: in.Ciphertext, + } + + plaintext, err := wrapping.EnvelopeDecrypt(envInfo, opt...) + if err != nil { + return nil, fmt.Errorf("error decrypting data with envelope: %w", err) + } + + return plaintext, nil +} + +// Client returns the IBM KP client used by the wrapper. +func (k *Wrapper) Client() *kp.Client { + return k.client +} + +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..d4a77e30 --- /dev/null +++ b/wrappers/ibmkp/ibmkp_acc_test.go @@ -0,0 +1,140 @@ +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, it is 8th field on CRN. +// - IBMCLOUD_KP_KEY_ID created on step 3 + +import ( + "context" + "crypto/subtle" + "os" + "testing" + + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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() + instanceID := os.Getenv(EnvIbmKpInstanceId) + os.Unsetenv(EnvIbmKpInstanceId) + + // Attempt to set config, expect failure due to missing config + _, err := s.SetConfig(context.Background()) + 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(context.Background()) + if err != nil { + t.Fatal(err) + } +} + +// This test does not need environment setup +func TestIbmKp_IgnoreEnv(t *testing.T) { + wrapper := NewWrapper() + client, _ := wrapper.GetIbmKpClient() + wrapper.client = client + + // Setup environment values to ignore for the following values + for _, envVar := range []string{EnvIbmApiKey, EnvIbmKpEndpoint, EnvIbmKpInstanceId, EnvIbmKpKeyId} { + oldVal := os.Getenv(envVar) + os.Setenv(envVar, "envValue") + defer os.Setenv(envVar, oldVal) + } + + config := map[string]string{ + "disallow_env_vars": "true", + "api_key": "a-api-key", + "instance_id": "a-instance-id", + "endpoint": "my-endpoint", + } + + _, err := wrapper.SetConfig(context.Background(), wrapping.WithConfigMap(config), wrapping.WithKeyId("a-key-key")) + assert.NoError(t, err) + + require.Equal(t, config["api_key"], wrapper.apiKey) + require.Equal(t, config["instance_id"], wrapper.instanceId) + require.Equal(t, "a-key-key", wrapper.keyId) + require.Equal(t, config["endpoint"], wrapper.endpoint) +} + +func TestIbmKp_Lifecycle(t *testing.T) { + checkAndSetEnvVars(t) + + s := NewWrapper() + _, err := s.SetConfig(context.Background()) + 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 subtle.ConstantTimeCompare(input, pt) == 0 { + 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() + + if os.Getenv("VAULT_ACC") == "" { + t.Skip("Skipping, env var 'VAULT_ACC' is empty") + } + + if os.Getenv(EnvIbmApiKey) == "" { + os.Setenv(EnvIbmApiKey, TestIbmApiKey) + } + + if os.Getenv(EnvIbmKpInstanceId) == "" { + os.Setenv(EnvIbmKpInstanceId, TestIbmKpInstanceId) + } + + if os.Getenv(EnvIbmKpKeyId) == "" { + os.Setenv(EnvIbmKpKeyId, TestIbmKpKeyId) + } +} diff --git a/wrappers/ibmkp/options.go b/wrappers/ibmkp/options.go new file mode 100644 index 00000000..34b4e3a2 --- /dev/null +++ b/wrappers/ibmkp/options.go @@ -0,0 +1,160 @@ +package ibmkp + +import ( + "strconv" + + "github.com/hashicorp/go-hclog" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" +) + +// getOpts iterates the inbound Options and returns a struct +func getOpts(opt ...wrapping.Option) (*options, error) { + // First, separate out options into local and global + opts := getDefaultOptions() + var wrappingOptions []wrapping.Option + var localOptions []OptionFunc + for _, o := range opt { + if o == nil { + continue + } + iface := o() + switch to := iface.(type) { + case wrapping.OptionFunc: + wrappingOptions = append(wrappingOptions, o) + case OptionFunc: + localOptions = append(localOptions, to) + } + } + + // Parse the global options + var err error + opts.Options, err = wrapping.GetOpts(wrappingOptions...) + if err != nil { + return nil, err + } + + // Don't ever return blank options + if opts.Options == nil { + opts.Options = new(wrapping.Options) + } + + // Local options can be provided either via the WithConfigMap field + // (for over the plugin barrier or embedding) or via local option functions + // (for embedding). First pull from the option. + if opts.WithConfigMap != nil { + for k, v := range opts.WithConfigMap { + switch k { + case "disallow_env_vars": + disallowEnvVars, err := strconv.ParseBool(v) + if err != nil { + return nil, err + } + opts.withDisallowEnvVars = disallowEnvVars + case "key_not_required": + keyNotRequired, err := strconv.ParseBool(v) + if err != nil { + return nil, err + } + opts.withKeyNotRequired = keyNotRequired + case "api_key": + opts.withApiKey = v + case "endpoint": + opts.withEndpoint = v + case "instance_id": + opts.withInstanceId = v + } + } + } + + // Now run the local options functions. This may overwrite options set by + // the options above. + for _, o := range localOptions { + if o != nil { + if err := o(&opts); err != nil { + return nil, err + } + } + } + + return &opts, nil +} + +// OptionFunc holds a function with local options +type OptionFunc func(*options) error + +// options = how options are represented +type options struct { + *wrapping.Options + + withDisallowEnvVars bool + withKeyNotRequired bool + withApiKey string + withEndpoint string + withInstanceId string + + withLogger hclog.Logger +} + +func getDefaultOptions() options { + return options{} +} + +// WithDisallowEnvVars provides a way to disable using env vars +func WithDisallowEnvVars(with bool) wrapping.Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withDisallowEnvVars = with + return nil + }) + } +} + +// WithKeyNotRequired provides a way to not require a key at config time +func WithKeyNotRequired(with bool) wrapping.Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withKeyNotRequired = with + return nil + }) + } +} + +// WithApiKey provides a way to choose the api_key +func WithApiKey(with string) wrapping.Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withApiKey = with + return nil + }) + } +} + +// WithEndpoint provides a way to choose the endpoint +func WithEndpoint(with string) wrapping.Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withEndpoint = with + return nil + }) + } +} + +// WithInstanceId provides a way to choose the instance_id +func WithInstanceId(with string) wrapping.Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withInstanceId = with + return nil + }) + } +} + +// WithLogger provides a way to pass in a logger +func WithLogger(with hclog.Logger) wrapping.Option { + return func() interface{} { + return OptionFunc(func(o *options) error { + o.withLogger = with + return nil + }) + } +}