1package gcpckms
2
3import (
4	"errors"
5	"fmt"
6	"os"
7	"sync/atomic"
8
9	cloudkms "cloud.google.com/go/kms/apiv1"
10	wrapping "github.com/hashicorp/go-kms-wrapping"
11	context "golang.org/x/net/context"
12	"google.golang.org/api/option"
13	kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
14)
15
16const (
17	// General GCP values, follows TF naming conventions
18	EnvGCPCKMSWrapperCredsPath = "GOOGLE_CREDENTIALS"
19	EnvGCPCKMSWrapperProject   = "GOOGLE_PROJECT"
20	EnvGCPCKMSWrapperLocation  = "GOOGLE_REGION"
21
22	// CKMS-specific values
23	EnvGCPCKMSWrapperKeyRing     = "GCPCKMS_WRAPPER_KEY_RING"
24	EnvVaultGCPCKMSSealKeyRing   = "VAULT_GCPCKMS_SEAL_KEY_RING"
25	EnvGCPCKMSWrapperCryptoKey   = "GCPCKMS_WRAPPER_CRYPTO_KEY"
26	EnvVaultGCPCKMSSealCryptoKey = "VAULT_GCPCKMS_SEAL_CRYPTO_KEY"
27)
28
29const (
30	// GCPKMSEncrypt is used to directly encrypt the data with KMS
31	GCPKMSEncrypt = iota
32	// GCPKMSEnvelopeAESGCMEncrypt is when a data encryption key is generatated and
33	// the data is encrypted with AESGCM and the key is encrypted with KMS
34	GCPKMSEnvelopeAESGCMEncrypt
35)
36
37type Wrapper struct {
38	// Values specific to IAM
39	credsPath string // Path to the creds file generated during service account creation
40
41	// Values specific to Cloud KMS service
42	project    string
43	location   string
44	keyRing    string
45	cryptoKey  string
46	parentName string // Parent path built from the above values
47
48	userAgent string
49
50	currentKeyID *atomic.Value
51
52	client *cloudkms.KeyManagementClient
53}
54
55var _ wrapping.Wrapper = (*Wrapper)(nil)
56
57func NewWrapper(opts *wrapping.WrapperOptions) *Wrapper {
58	if opts == nil {
59		opts = new(wrapping.WrapperOptions)
60	}
61	s := &Wrapper{
62		currentKeyID: new(atomic.Value),
63	}
64	s.currentKeyID.Store("")
65	return s
66}
67
68// SetConfig sets the fields on the Wrapper object based on values from the
69// config parameter. Environment variables take precedence over values provided
70// in the config struct.
71//
72// Order of precedence for GCP credentials file:
73// * GOOGLE_CREDENTIALS environment variable
74// * `credentials` value from Value configuration file
75// * GOOGLE_APPLICATION_CREDENTIALS (https://developers.google.com/identity/protocols/application-default-credentials)
76func (s *Wrapper) SetConfig(config map[string]string) (map[string]string, error) {
77	if config == nil {
78		config = map[string]string{}
79	}
80
81	s.userAgent = config["user_agent"]
82
83	// Do not return an error in this case. Let client initialization in
84	// getClient() attempt to sort out where to get default credentials internally
85	// within the SDK (e.g. checking for GOOGLE_APPLICATION_CREDENTIALS), and let
86	// it error out there if none is found. This is here to establish precedence on
87	// non-default input methods.
88	switch {
89	case os.Getenv(EnvGCPCKMSWrapperCredsPath) != "":
90		s.credsPath = os.Getenv(EnvGCPCKMSWrapperCredsPath)
91	case config["credentials"] != "":
92		s.credsPath = config["credentials"]
93	}
94
95	switch {
96	case os.Getenv(EnvGCPCKMSWrapperProject) != "":
97		s.project = os.Getenv(EnvGCPCKMSWrapperProject)
98	case config["project"] != "":
99		s.project = config["project"]
100	default:
101		return nil, errors.New("'project' not found for GCP CKMS wrapper configuration")
102	}
103
104	switch {
105	case os.Getenv(EnvGCPCKMSWrapperLocation) != "":
106		s.location = os.Getenv(EnvGCPCKMSWrapperLocation)
107	case config["region"] != "":
108		s.location = config["region"]
109	default:
110		return nil, errors.New("'region' not found for GCP CKMS wrapper configuration")
111	}
112
113	switch {
114	case os.Getenv(EnvGCPCKMSWrapperKeyRing) != "":
115		s.keyRing = os.Getenv(EnvGCPCKMSWrapperKeyRing)
116	case os.Getenv(EnvVaultGCPCKMSSealKeyRing) != "":
117		s.keyRing = os.Getenv(EnvVaultGCPCKMSSealKeyRing)
118	case config["key_ring"] != "":
119		s.keyRing = config["key_ring"]
120	default:
121		return nil, errors.New("'key_ring' not found for GCP CKMS wrapper configuration")
122	}
123
124	switch {
125	case os.Getenv(EnvGCPCKMSWrapperCryptoKey) != "":
126		s.cryptoKey = os.Getenv(EnvGCPCKMSWrapperCryptoKey)
127	case os.Getenv(EnvVaultGCPCKMSSealCryptoKey) != "":
128		s.cryptoKey = os.Getenv(EnvVaultGCPCKMSSealCryptoKey)
129	case config["crypto_key"] != "":
130		s.cryptoKey = config["crypto_key"]
131	default:
132		return nil, errors.New("'crypto_key' not found for GCP CKMS wrapper configuration")
133	}
134
135	// Set the parent name for encrypt/decrypt requests
136	s.parentName = fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", s.project, s.location, s.keyRing, s.cryptoKey)
137
138	// Set and check s.client
139	if s.client == nil {
140		kmsClient, err := s.getClient()
141		if err != nil {
142			return nil, fmt.Errorf("error initializing GCP CKMS wrapper client: %w", err)
143		}
144		s.client = kmsClient
145
146		// Make sure user has permissions to encrypt (also checks if key exists)
147		ctx := context.Background()
148		if _, err := s.Encrypt(ctx, []byte("vault-gcpckms-test"), nil); err != nil {
149			return nil, fmt.Errorf("failed to encrypt with GCP CKMS - ensure the "+
150				"key exists and the service account has at least "+
151				"roles/cloudkms.cryptoKeyEncrypterDecrypter permission: %w", err)
152		}
153	}
154
155	// Map that holds non-sensitive configuration info to return
156	wrapperInfo := make(map[string]string)
157	wrapperInfo["project"] = s.project
158	wrapperInfo["region"] = s.location
159	wrapperInfo["key_ring"] = s.keyRing
160	wrapperInfo["crypto_key"] = s.cryptoKey
161
162	return wrapperInfo, nil
163}
164
165// Init is called during core.Initialize. No-op at the moment
166func (s *Wrapper) Init(_ context.Context) error {
167	return nil
168}
169
170// Finalize is called during shutdown. This is a no-op since
171// Wrapper doesn't require any cleanup.
172func (s *Wrapper) Finalize(_ context.Context) error {
173	return nil
174}
175
176// Type returns the type for this particular wrapper implementation
177func (s *Wrapper) Type() string {
178	return wrapping.GCPCKMS
179}
180
181// KeyID returns the last known key id
182func (s *Wrapper) KeyID() string {
183	return s.currentKeyID.Load().(string)
184}
185
186// HMACKeyID returns the last known key id
187func (s *Wrapper) HMACKeyID() string {
188	return ""
189}
190
191// Encrypt is used to encrypt the master key using the the AWS CMK.
192// This returns the ciphertext, and/or any errors from this
193// call. This should be called after s.client has been instantiated.
194func (s *Wrapper) Encrypt(ctx context.Context, plaintext, aad []byte) (blob *wrapping.EncryptedBlobInfo, err error) {
195	if plaintext == nil {
196		return nil, errors.New("given plaintext for encryption is nil")
197	}
198
199	env, err := wrapping.NewEnvelope(nil).Encrypt(plaintext, aad)
200	if err != nil {
201		return nil, fmt.Errorf("error wrapping data: %w", err)
202	}
203
204	resp, err := s.client.Encrypt(ctx, &kmspb.EncryptRequest{
205		Name:      s.parentName,
206		Plaintext: env.Key,
207	})
208	if err != nil {
209		return nil, err
210	}
211
212	// Store current key id value
213	s.currentKeyID.Store(resp.Name)
214
215	ret := &wrapping.EncryptedBlobInfo{
216		Ciphertext: env.Ciphertext,
217		IV:         env.IV,
218		KeyInfo: &wrapping.KeyInfo{
219			Mechanism: GCPKMSEnvelopeAESGCMEncrypt,
220			// Even though we do not use the key id during decryption, store it
221			// to know exactly what version was used in encryption in case we
222			// want to rewrap older entries
223			KeyID:      resp.Name,
224			WrappedKey: resp.Ciphertext,
225		},
226	}
227
228	return ret, nil
229}
230
231// Decrypt is used to decrypt the ciphertext.
232func (s *Wrapper) Decrypt(ctx context.Context, in *wrapping.EncryptedBlobInfo, aad []byte) (pt []byte, err error) {
233	if in.Ciphertext == nil {
234		return nil, fmt.Errorf("given ciphertext for decryption is nil")
235	}
236
237	// Default to mechanism used before key info was stored
238	if in.KeyInfo == nil {
239		in.KeyInfo = &wrapping.KeyInfo{
240			Mechanism: GCPKMSEncrypt,
241		}
242	}
243
244	var plaintext []byte
245	switch in.KeyInfo.Mechanism {
246	case GCPKMSEncrypt:
247		resp, err := s.client.Decrypt(ctx, &kmspb.DecryptRequest{
248			Name:       s.parentName,
249			Ciphertext: in.Ciphertext,
250		})
251		if err != nil {
252			return nil, fmt.Errorf("failed to decrypt data: %w", err)
253		}
254
255		plaintext = resp.Plaintext
256
257	case GCPKMSEnvelopeAESGCMEncrypt:
258		resp, err := s.client.Decrypt(ctx, &kmspb.DecryptRequest{
259			Name:       s.parentName,
260			Ciphertext: in.KeyInfo.WrappedKey,
261		})
262		if err != nil {
263			return nil, fmt.Errorf("failed to decrypt envelope: %w", err)
264		}
265
266		envInfo := &wrapping.EnvelopeInfo{
267			Key:        resp.Plaintext,
268			IV:         in.IV,
269			Ciphertext: in.Ciphertext,
270		}
271		plaintext, err = wrapping.NewEnvelope(nil).Decrypt(envInfo, aad)
272		if err != nil {
273			return nil, fmt.Errorf("error decrypting data with envelope: %w", err)
274		}
275
276	default:
277		return nil, fmt.Errorf("invalid mechanism: %d", in.KeyInfo.Mechanism)
278	}
279
280	return plaintext, nil
281}
282
283func (s *Wrapper) getClient() (*cloudkms.KeyManagementClient, error) {
284	client, err := cloudkms.NewKeyManagementClient(context.Background(),
285		option.WithCredentialsFile(s.credsPath),
286		option.WithUserAgent(s.userAgent),
287	)
288	if err != nil {
289		return nil, fmt.Errorf("failed to create KMS client: %w", err)
290	}
291
292	return client, nil
293}
294