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