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