@@ -2,8 +2,18 @@ package gcpkms // import "go.mozilla.org/sops/v3/gcpkms"
22
33import (
44 "context"
5+ "crypto/rand"
6+ "crypto/rsa"
7+ "crypto/sha1"
8+ "crypto/sha256"
9+ "crypto/sha512"
10+ "crypto/x509"
511 "encoding/base64"
12+ "encoding/pem"
13+ "errors"
614 "fmt"
15+ "hash"
16+ "hash/crc32"
717 "os"
818 "regexp"
919 "strings"
@@ -14,6 +24,7 @@ import (
1424 "google.golang.org/api/option"
1525 kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
1626 "google.golang.org/grpc"
27+ "google.golang.org/protobuf/types/known/wrapperspb"
1728
1829 "go.mozilla.org/sops/v3/logging"
1930)
@@ -103,11 +114,53 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
103114 }
104115 }()
105116
117+ ctx := context .Background ()
118+ purpose , err := key .purpose (ctx , service )
119+ if err != nil {
120+ return err
121+ }
122+
123+ switch purpose {
124+ case kmspb .CryptoKey_ENCRYPT_DECRYPT :
125+ return key .encryptSymmetric (ctx , service , dataKey )
126+ case kmspb .CryptoKey_ASYMMETRIC_DECRYPT :
127+ return key .encryptAsymmetric (ctx , service , dataKey )
128+ default :
129+ log .WithField ("resourceID" , key .ResourceID ).WithField ("purpose" , purpose .String ()).Error ("This key is not for encryption" )
130+ return fmt .Errorf ("this key is not for encryption, purpose: %v" , purpose .String ())
131+ }
132+ }
133+
134+ func (key * MasterKey ) purpose (ctx context.Context , service * kms.KeyManagementClient ) (kmspb.CryptoKey_CryptoKeyPurpose , error ) {
135+ req := & kmspb.GetCryptoKeyRequest {
136+ Name : key .resourceIDWithoutVersion (),
137+ }
138+ cryptoKey , err := service .GetCryptoKey (ctx , req )
139+ if err != nil {
140+ log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Error ("Get key metadata failed" )
141+ return kmspb .CryptoKey_CRYPTO_KEY_PURPOSE_UNSPECIFIED , fmt .Errorf ("failed to get key metadata from GCP KMS service: %w" , err )
142+ }
143+
144+ return cryptoKey .GetPurpose (), nil
145+ }
146+
147+ // assume key.ResourceID is in following format
148+ // - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key`
149+ // - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key/cryptoKeyVersions/version`
150+ func (key MasterKey ) resourceIDWithoutVersion () string {
151+ re := regexp .MustCompile (`^(projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+)(?:/cryptoKeyVersions/[^/]+)?$` )
152+ matches := re .FindStringSubmatch (key .ResourceID )
153+ if len (matches ) < 2 {
154+ return ""
155+ }
156+ return matches [1 ]
157+ }
158+
159+ func (key * MasterKey ) encryptSymmetric (ctx context.Context , service * kms.KeyManagementClient , dataKey []byte ) error {
106160 req := & kmspb.EncryptRequest {
107161 Name : key .ResourceID ,
108162 Plaintext : dataKey ,
109163 }
110- ctx := context .Background ()
111164 resp , err := service .Encrypt (ctx , req )
112165 if err != nil {
113166 log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Error ("Encryption failed" )
@@ -117,7 +170,70 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
117170 // The previous GCP KMS client used to work with base64 encoded
118171 // strings.
119172 key .EncryptedKey = base64 .StdEncoding .EncodeToString (resp .Ciphertext )
120- log .WithField ("resourceID" , key .ResourceID ).Info ("Encryption succeeded" )
173+ log .WithField ("resourceID" , key .ResourceID ).Info ("Symmetric encryption succeeded" )
174+ return nil
175+ }
176+
177+ func (key * MasterKey ) encryptAsymmetric (ctx context.Context , service * kms.KeyManagementClient , dataKey []byte ) error {
178+ req := & kmspb.GetPublicKeyRequest {
179+ Name : key .ResourceID ,
180+ }
181+ resp , err := service .GetPublicKey (ctx , req )
182+ if err != nil {
183+ log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Error ("Get public key failed" )
184+ return fmt .Errorf ("failed to get public key from GCP KMS service: %w" , err )
185+ }
186+
187+ if resp .GetPemCrc32C ().GetValue () != wrapperspb .Int64 (int64 (crc32c ([]byte (resp .GetPem ())))).Value {
188+ log .WithField ("resourceID" , key .ResourceID ).Error ("Get public key response corrupted in-transit" )
189+ return errors .New ("get public key response corrupted in-transit" )
190+ }
191+
192+ block , _ := pem .Decode ([]byte (resp .Pem ))
193+ publicKey , err := x509 .ParsePKIXPublicKey (block .Bytes )
194+ if err != nil {
195+ log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Info ("Failed to parse public key" )
196+ return fmt .Errorf ("Failed to parse public key: %w" , err )
197+ }
198+ rsaKey , ok := publicKey .(* rsa.PublicKey )
199+ if ! ok {
200+ log .WithField ("resourceID" , key .ResourceID ).Info ("Public key is not RSA" )
201+ return errors .New ("public key is not RSA" )
202+ }
203+
204+ var hash hash.Hash
205+
206+ switch resp .GetAlgorithm () {
207+ case kmspb .CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA256 :
208+ hash = sha256 .New ()
209+ case kmspb .CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA256 :
210+ hash = sha256 .New ()
211+ case kmspb .CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA256 :
212+ hash = sha256 .New ()
213+ case kmspb .CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA512 :
214+ hash = sha512 .New ()
215+ case kmspb .CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA1 :
216+ hash = sha1 .New ()
217+ case kmspb .CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA1 :
218+ hash = sha1 .New ()
219+ case kmspb .CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA1 :
220+ hash = sha1 .New ()
221+ default :
222+ log .WithField ("resourceID" , key .ResourceID ).WithField ("algorithm" , resp .GetAlgorithm ().String ()).Error ("Unsupported algorithm" )
223+ return fmt .Errorf ("Key with unsupported algorithm: %s" , resp .GetAlgorithm ().String ())
224+ }
225+
226+ ciphertext , err := rsa .EncryptOAEP (hash , rand .Reader , rsaKey , dataKey , nil )
227+ if err != nil {
228+ log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Error ("rsa.EncryptOAEP() error" )
229+ return fmt .Errorf ("rsa.EncryptOAEP: %w" , err )
230+ }
231+
232+ // NB: base64 encoding is for compatibility with SOPS <=3.8.x.
233+ // The previous GCP KMS client used to work with base64 encoded
234+ // strings.
235+ key .EncryptedKey = base64 .StdEncoding .EncodeToString (ciphertext )
236+ log .WithField ("resourceID" , key .ResourceID ).Info ("Asymmetric encryption succeeded" )
121237 return nil
122238}
123239
@@ -162,18 +278,51 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
162278 return nil , err
163279 }
164280
281+ ctx := context .Background ()
282+
283+ purpose , err := key .purpose (ctx , service )
284+ if err != nil {
285+ return nil , err
286+ }
287+
288+ switch purpose {
289+ case kmspb .CryptoKey_ENCRYPT_DECRYPT :
290+ return key .decryptSymmetric (ctx , service , decodedCipher )
291+ case kmspb .CryptoKey_ASYMMETRIC_DECRYPT :
292+ return key .decryptAsymmetric (ctx , service , decodedCipher )
293+ default :
294+ log .WithField ("resourceID" , key .ResourceID ).WithField ("purpose" , purpose .String ()).Info ("This key cannot be used for decryption" )
295+ return nil , fmt .Errorf ("This key cannot be used for decryption, purpose: %s" , purpose .String ())
296+ }
297+ }
298+
299+ func (key * MasterKey ) decryptSymmetric (ctx context.Context , service * kms.KeyManagementClient , decodedCipher []byte ) ([]byte , error ) {
165300 req := & kmspb.DecryptRequest {
166301 Name : key .ResourceID ,
167302 Ciphertext : decodedCipher ,
168303 }
169- ctx := context .Background ()
170304 resp , err := service .Decrypt (ctx , req )
171305 if err != nil {
172- log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Error ("Decryption failed" )
306+ log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Error ("Symmetric decryption failed" )
307+ return nil , fmt .Errorf ("failed to decrypt sops data key with GCP KMS key: %w" , err )
308+ }
309+
310+ log .WithField ("resourceID" , key .ResourceID ).Info ("Symmetric decryption succeeded" )
311+ return resp .Plaintext , nil
312+ }
313+
314+ func (key * MasterKey ) decryptAsymmetric (ctx context.Context , service * kms.KeyManagementClient , decodedCipher []byte ) ([]byte , error ) {
315+ req := & kmspb.AsymmetricDecryptRequest {
316+ Name : key .ResourceID ,
317+ Ciphertext : decodedCipher ,
318+ }
319+ resp , err := service .AsymmetricDecrypt (ctx , req )
320+ if err != nil {
321+ log .WithError (err ).WithField ("resourceID" , key .ResourceID ).Error ("Asymmetric decryption failed" )
173322 return nil , fmt .Errorf ("failed to decrypt sops data key with GCP KMS key: %w" , err )
174323 }
175324
176- log .WithField ("resourceID" , key .ResourceID ).Info ("Decryption succeeded" )
325+ log .WithField ("resourceID" , key .ResourceID ).Info ("Asymmetric decryption succeeded" )
177326 return resp .Plaintext , nil
178327}
179328
@@ -201,7 +350,7 @@ func (key MasterKey) ToMap() map[string]interface{} {
201350// It returns an error if the ResourceID is invalid, or if the setup of the
202351// client fails.
203352func (key * MasterKey ) newKMSClient () (* kms.KeyManagementClient , error ) {
204- re := regexp .MustCompile (`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$` )
353+ re := regexp .MustCompile (`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+(?:/cryptoKeyVersions/[^/]+)? $` )
205354 matches := re .FindStringSubmatch (key .ResourceID )
206355 if matches == nil {
207356 return nil , fmt .Errorf ("no valid resource ID found in %q" , key .ResourceID )
@@ -216,8 +365,8 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) {
216365 if err != nil {
217366 return nil , err
218367 }
219- if credentials != nil {
220- opts = append (opts , option .WithCredentialsJSON (key . credentialJSON ))
368+ if len ( credentials ) > 0 {
369+ opts = append (opts , option .WithCredentialsJSON (credentials ))
221370 }
222371 }
223372 if key .grpcConn != nil {
@@ -244,3 +393,8 @@ func getGoogleCredentials() ([]byte, error) {
244393 }
245394 return []byte (defaultCredentials ), nil
246395}
396+
397+ func crc32c (data []byte ) uint32 {
398+ t := crc32 .MakeTable (crc32 .Castagnoli )
399+ return crc32 .Checksum (data , t )
400+ }
0 commit comments