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