1package huaweicloudkms 2 3import ( 4 "context" 5 "crypto/tls" 6 "encoding/base64" 7 "fmt" 8 "os" 9 "sync/atomic" 10 11 "github.com/hashicorp/go-cleanhttp" 12 wrapping "github.com/hashicorp/go-kms-wrapping" 13 "github.com/huaweicloud/golangsdk" 14 huaweisdk "github.com/huaweicloud/golangsdk/openstack" 15 kmsKeys "github.com/huaweicloud/golangsdk/openstack/kms/v1/keys" 16) 17 18// These constants contain the accepted env vars; the Vault one is for backwards compat 19const ( 20 EnvHuaweiCloudKMSWrapperKeyID = "HUAWEICLOUDKMS_WRAPPER_KEY_ID" 21) 22 23// Wrapper is a Wrapper that uses HuaweiCloud's KMS 24type Wrapper struct { 25 client kmsClient 26 keyID string 27 currentKeyID *atomic.Value 28} 29 30// Ensure that we are implementing Wrapper 31var _ wrapping.Wrapper = (*Wrapper)(nil) 32 33// NewWrapper creates a new HuaweiCloud Wrapper 34func NewWrapper(opts *wrapping.WrapperOptions) *Wrapper { 35 if opts == nil { 36 opts = new(wrapping.WrapperOptions) 37 } 38 k := &Wrapper{ 39 currentKeyID: new(atomic.Value), 40 } 41 k.currentKeyID.Store("") 42 return k 43} 44 45// SetConfig sets the fields on the HuaweiCloudKMSWrapper object based on 46// values from the config parameter. 47// 48// Order of precedence HuaweiCloud values: 49// * Environment variable 50// * Value from Vault configuration file 51// * Instance metadata role (access key and secret key) 52func (k *Wrapper) SetConfig(config map[string]string) (map[string]string, error) { 53 if config == nil { 54 config = map[string]string{} 55 } 56 57 // Check and set KeyID 58 keyID, err := getConfig( 59 "kms_key_id", 60 os.Getenv(EnvHuaweiCloudKMSWrapperKeyID), 61 config["kms_key_id"]) 62 if err != nil { 63 return nil, err 64 } 65 k.keyID = keyID 66 67 if k.client == nil { 68 client, err := buildKMSClient(config) 69 if err != nil { 70 return nil, err 71 } 72 k.client = client 73 } 74 75 // Test the client connection using provided key ID 76 keyInfo, err := k.client.describeKey(k.keyID) 77 if err != nil { 78 return nil, fmt.Errorf("error fetching HuaweiCloud KMS key information: %w", err) 79 } 80 81 // Store the current key id. If using a key alias, this will point to the actual 82 // unique key that that was used for this encrypt operation. 83 k.currentKeyID.Store(keyInfo.KeyID) 84 85 // Map that holds non-sensitive configuration info 86 wrapperInfo := make(map[string]string) 87 wrapperInfo["region"] = k.client.getRegion() 88 wrapperInfo["project"] = k.client.getProject() 89 wrapperInfo["kms_key_id"] = k.keyID 90 91 return wrapperInfo, nil 92} 93 94// Init is called during core.Initialize. No-op at the moment. 95func (k *Wrapper) Init(_ context.Context) error { 96 return nil 97} 98 99// Finalize is called during shutdown. This is a no-op since 100// HuaweiCloudKMSWrapper doesn't require any cleanup. 101func (k *Wrapper) Finalize(_ context.Context) error { 102 return nil 103} 104 105// Type returns the type for this particular wrapper implementation 106func (k *Wrapper) Type() string { 107 return wrapping.HuaweiCloudKMS 108} 109 110// KeyID returns the last known key id 111func (k *Wrapper) KeyID() string { 112 return k.currentKeyID.Load().(string) 113} 114 115// HMACKeyID returns nothing, it's here to satisfy the interface 116func (k *Wrapper) HMACKeyID() string { 117 return "" 118} 119 120// Encrypt is used to encrypt the master key using the the HuaweiCloud CMK. 121// This returns the ciphertext, and/or any errors from this 122// call. This should be called after the KMS client has been instantiated. 123func (k *Wrapper) Encrypt(_ context.Context, plaintext, aad []byte) (blob *wrapping.EncryptedBlobInfo, err error) { 124 if plaintext == nil { 125 return nil, fmt.Errorf("given plaintext for encryption is nil") 126 } 127 128 env, err := wrapping.NewEnvelope(nil).Encrypt(plaintext, aad) 129 if err != nil { 130 return nil, fmt.Errorf("error wrapping data: %w", err) 131 } 132 133 output, err := k.client.encrypt(k.keyID, base64.StdEncoding.EncodeToString(env.Key)) 134 if err != nil { 135 return nil, fmt.Errorf("error encrypting data: %w", err) 136 } 137 138 // Store the current key id. 139 keyID := output.KeyID 140 k.currentKeyID.Store(keyID) 141 142 blob = &wrapping.EncryptedBlobInfo{ 143 Ciphertext: env.Ciphertext, 144 IV: env.IV, 145 KeyInfo: &wrapping.KeyInfo{ 146 KeyID: keyID, 147 WrappedKey: []byte(output.Ciphertext), 148 }, 149 } 150 151 return blob, nil 152} 153 154// Decrypt is used to decrypt the ciphertext. This should be called after Init. 155func (k *Wrapper) Decrypt(_ context.Context, in *wrapping.EncryptedBlobInfo, aad []byte) (pt []byte, err error) { 156 if in == nil { 157 return nil, fmt.Errorf("given input for decryption is nil") 158 } 159 160 // KeyID is not passed to this call because HuaweiCloud handles this 161 // internally based on the metadata stored with the encrypted data 162 plainText, err := k.client.decrypt(string(in.KeyInfo.WrappedKey)) 163 if err != nil { 164 return nil, fmt.Errorf("error decrypting data encryption key: %w", err) 165 } 166 167 keyBytes, err := base64.StdEncoding.DecodeString(plainText) 168 if err != nil { 169 return nil, err 170 } 171 172 envInfo := &wrapping.EnvelopeInfo{ 173 Key: keyBytes, 174 IV: in.IV, 175 Ciphertext: in.Ciphertext, 176 } 177 pt, err = wrapping.NewEnvelope(nil).Decrypt(envInfo, aad) 178 if err != nil { 179 return nil, fmt.Errorf("error decrypting data: %w", err) 180 } 181 return 182} 183 184func getConfig(name string, values ...string) (string, error) { 185 for _, v := range values { 186 if "" != v { 187 return v, nil 188 } 189 } 190 191 return "", fmt.Errorf("'%s' not found for HuaweiCloud KMS wrapper configuration", name) 192} 193 194func buildKMSClient(config map[string]string) (kmsClient, error) { 195 // Check and set region. 196 region, err := getConfig("region", os.Getenv("HUAWEICLOUD_REGION"), config["region"]) 197 if err != nil { 198 return nil, err 199 } 200 201 // Check and set project. 202 project, err := getConfig("project", os.Getenv("HUAWEICLOUD_PROJECT"), config["project"]) 203 if err != nil { 204 return nil, err 205 } 206 207 // Check and set access key. 208 accessKey, err := getConfig("access_key", os.Getenv("HUAWEICLOUD_ACCESS_KEY"), config["access_key"]) 209 if err != nil { 210 return nil, err 211 } 212 213 // Check and set project. 214 secretKey, err := getConfig("secret_key", os.Getenv("HUAWEICLOUD_SECRET_KEY"), config["secret_key"]) 215 if err != nil { 216 return nil, err 217 } 218 219 // Check and set endpoint. 220 endpoint, _ := getConfig( 221 "identity_endpoint", 222 os.Getenv("HUAWEICLOUD_IDENTITY_ENDPOINT"), 223 config["identity_endpoint"], 224 "https://iam.myhwclouds.com:443/v3") 225 226 option := golangsdk.AKSKAuthOptions{ 227 Region: region, 228 ProjectName: project, 229 AccessKey: accessKey, 230 SecretKey: secretKey, 231 IdentityEndpoint: endpoint, 232 } 233 234 client, err := buildServiceClient(option) 235 if err != nil { 236 return nil, err 237 } 238 239 return &kmsClientImpl{region: region, project: project, client: client}, nil 240} 241 242func buildServiceClient(option golangsdk.AKSKAuthOptions) (*golangsdk.ServiceClient, error) { 243 client, err := huaweisdk.NewClient(option.IdentityEndpoint) 244 if err != nil { 245 return nil, err 246 } 247 248 transport := cleanhttp.DefaultTransport() 249 transport.TLSClientConfig = &tls.Config{} 250 client.HTTPClient.Transport = transport 251 252 err = huaweisdk.Authenticate(client, option) 253 if err != nil { 254 return nil, err 255 } 256 257 return huaweisdk.NewKMSV1(client, golangsdk.EndpointOpts{ 258 Region: option.Region, 259 Availability: golangsdk.AvailabilityPublic, 260 }) 261} 262 263type encryptResponse struct { 264 KeyID string `json:"key_id"` 265 Ciphertext string `json:"cipher_text"` 266} 267 268type kmsClient interface { 269 getRegion() string 270 getProject() string 271 describeKey(keyID string) (*kmsKeys.Key, error) 272 encrypt(keyID, plainText string) (encryptResponse, error) 273 decrypt(cipherText string) (string, error) 274} 275 276type kmsClientImpl struct { 277 region string 278 project string 279 client *golangsdk.ServiceClient 280} 281 282func (c *kmsClientImpl) getRegion() string { 283 return c.region 284} 285 286func (c *kmsClientImpl) getProject() string { 287 return c.project 288} 289 290func (c *kmsClientImpl) describeKey(keyID string) (*kmsKeys.Key, error) { 291 return kmsKeys.Get(c.client, keyID).ExtractKeyInfo() 292} 293 294func (c *kmsClientImpl) encrypt(keyID, plainText string) (encryptResponse, error) { 295 url := c.client.ServiceURL(c.client.ProjectID, "kms", "encrypt-data") 296 r := golangsdk.Result{} 297 _, r.Err = c.client.Post( 298 url, 299 &map[string]interface{}{"key_id": keyID, "plain_text": plainText}, 300 &r.Body, 301 &golangsdk.RequestOpts{ 302 OkCodes: []int{200}, 303 MoreHeaders: map[string]string{"Content-Type": "application/json"}, 304 }) 305 306 resp := encryptResponse{} 307 err := r.ExtractInto(&resp) 308 if err != nil { 309 return resp, fmt.Errorf("error encrypting data: %s", err) 310 } 311 return resp, nil 312} 313 314func (c *kmsClientImpl) decrypt(cipherText string) (string, error) { 315 url := c.client.ServiceURL(c.client.ProjectID, "kms", "decrypt-data") 316 r := golangsdk.Result{} 317 _, r.Err = c.client.Post( 318 url, 319 &map[string]interface{}{"cipher_text": cipherText}, 320 &r.Body, 321 &golangsdk.RequestOpts{ 322 OkCodes: []int{200}, 323 MoreHeaders: map[string]string{"Content-Type": "application/json"}, 324 }) 325 326 var resp struct { 327 PlainText string `json:"plain_text"` 328 } 329 err := r.ExtractInto(&resp) 330 if err != nil { 331 return "", fmt.Errorf("error decrypting data: %s", err) 332 } 333 334 return resp.PlainText, nil 335} 336