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