11package gcpkms //import "go.mozilla.org/sops/v3/gcpkms"
22
33import (
4+ "crypto/rand"
5+ "crypto/rsa"
6+ "crypto/sha1"
7+ "crypto/sha256"
8+ "crypto/sha512"
9+ "crypto/x509"
410 "encoding/base64"
11+ "encoding/pem"
12+ "errors"
513 "fmt"
6- "google.golang.org/api/option"
14+ "hash"
15+ "hash/crc32"
716 "os"
817 "regexp"
918 "strings"
@@ -14,6 +23,7 @@ import (
1423 "github.com/sirupsen/logrus"
1524 "golang.org/x/net/context"
1625 cloudkms "google.golang.org/api/cloudkms/v1"
26+ "google.golang.org/api/option"
1727)
1828
1929var log * logrus.Logger
@@ -46,6 +56,37 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
4656 log .WithField ("resourceID" , key .ResourceID ).Info ("Encryption failed" )
4757 return fmt .Errorf ("Cannot create GCP KMS service: %w" , err )
4858 }
59+
60+ purpose , err := key .purpose (cloudkmsService )
61+ if err != nil {
62+ return err
63+ }
64+
65+ switch purpose {
66+ case "ENCRYPT_DECRYPT" :
67+ return key .encryptSymmetric (cloudkmsService , dataKey )
68+ case "ASYMMETRIC_DECRYPT" :
69+ return key .encryptAsymmetric (cloudkmsService , dataKey )
70+ }
71+
72+ log .WithField ("resourceID" , key .ResourceID ).WithField ("purpose" , purpose ).Info ("This key cannot be used for encryption" )
73+ return fmt .Errorf ("This key cannot be used for encryption, purpose: %s" , purpose )
74+ }
75+
76+ func (key * MasterKey ) purpose (cloudkmsService * cloudkms.Service ) (string , error ) {
77+ // It needs to use `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key` to request.
78+ // If request with format `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key/cryptoKeyVersions/version`,
79+ // KMS will response without `purpose`.
80+ resp , err := cloudkmsService .Projects .Locations .KeyRings .CryptoKeys .Get (key .resourceIDWithoutVersion ()).Do ()
81+ if err != nil {
82+ log .WithField ("resourceID" , key .ResourceID ).Info ("Get key info failed" )
83+ return "" , fmt .Errorf ("Get key from GCP KMS failed: %w" , err )
84+ }
85+
86+ return resp .Purpose , nil
87+ }
88+
89+ func (key * MasterKey ) encryptSymmetric (cloudkmsService * cloudkms.Service , dataKey []byte ) error {
4990 req := & cloudkms.EncryptRequest {
5091 Plaintext : base64 .StdEncoding .EncodeToString (dataKey ),
5192 }
@@ -59,6 +100,63 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
59100 return nil
60101}
61102
103+ func (key * MasterKey ) encryptAsymmetric (cloudkmsService * cloudkms.Service , dataKey []byte ) error {
104+ resp , err := cloudkmsService .Projects .Locations .KeyRings .CryptoKeys .CryptoKeyVersions .GetPublicKey (key .ResourceID ).Do ()
105+ if err != nil {
106+ log .WithField ("resourceID" , key .ResourceID ).Info ("Get public key failed" )
107+ return fmt .Errorf ("Get public key from GCP KMS failed: %w" , err )
108+ }
109+
110+ if resp .PemCrc32c != int64 (crc32c ([]byte (resp .Pem ))) {
111+ log .WithField ("resourceID" , key .ResourceID ).Info ("Get public key response corrupted in-transit" )
112+ return errors .New ("Get public key response corrupted in-transit" )
113+ }
114+
115+ block , _ := pem .Decode ([]byte (resp .Pem ))
116+ publicKey , err := x509 .ParsePKIXPublicKey (block .Bytes )
117+ if err != nil {
118+ log .WithField ("resourceID" , key .ResourceID ).Info ("Failed to parse public key" )
119+ return fmt .Errorf ("Failed to parse public key: %w" , err )
120+ }
121+ rsaKey , ok := publicKey .(* rsa.PublicKey )
122+ if ! ok {
123+ log .WithField ("resourceID" , key .ResourceID ).Info ("Public key is not RSA" )
124+ return errors .New ("Public key is not RSA" )
125+ }
126+
127+ var hash hash.Hash
128+
129+ switch resp .Algorithm {
130+ case "RSA_DECRYPT_OAEP_2048_SHA256" :
131+ hash = sha256 .New ()
132+ case "RSA_DECRYPT_OAEP_3072_SHA256" :
133+ hash = sha256 .New ()
134+ case "RSA_DECRYPT_OAEP_4096_SHA256" :
135+ hash = sha256 .New ()
136+ case "RSA_DECRYPT_OAEP_4096_SHA512" :
137+ hash = sha512 .New ()
138+ case "RSA_DECRYPT_OAEP_2048_SHA1" :
139+ hash = sha1 .New ()
140+ case "RSA_DECRYPT_OAEP_3072_SHA1" :
141+ hash = sha1 .New ()
142+ case "RSA_DECRYPT_OAEP_4096_SHA1" :
143+ hash = sha1 .New ()
144+ default :
145+ log .WithField ("resourceID" , key .ResourceID ).WithField ("algorithm" , resp .Algorithm ).Info ("Unsupported algorithm" )
146+ return fmt .Errorf ("Key with unsupported algorithm: %s" , resp .Algorithm )
147+ }
148+
149+ ciphertext , err := rsa .EncryptOAEP (hash , rand .Reader , rsaKey , dataKey , nil )
150+ if err != nil {
151+ log .WithField ("resourceID" , key .ResourceID ).Info ("rsa.EncryptOAEP() error" )
152+ return fmt .Errorf ("rsa.EncryptOAEP: %w" , err )
153+ }
154+
155+ key .EncryptedKey = base64 .StdEncoding .EncodeToString (ciphertext )
156+
157+ return nil
158+ }
159+
62160// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet
63161func (key * MasterKey ) EncryptIfNeeded (dataKey []byte ) error {
64162 if key .EncryptedKey == "" {
@@ -75,6 +173,23 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
75173 return nil , fmt .Errorf ("Cannot create GCP KMS service: %w" , err )
76174 }
77175
176+ purpose , err := key .purpose (cloudkmsService )
177+ if err != nil {
178+ return nil , err
179+ }
180+
181+ switch purpose {
182+ case "ENCRYPT_DECRYPT" :
183+ return key .decryptSymmetric (cloudkmsService )
184+ case "ASYMMETRIC_DECRYPT" :
185+ return key .decryptAsymmetric (cloudkmsService )
186+ default :
187+ log .WithField ("resourceID" , key .ResourceID ).WithField ("purpose" , purpose ).Info ("This key cannot be used for decryption" )
188+ return nil , fmt .Errorf ("This key cannot be used for decryption, purpose: %s" , purpose )
189+ }
190+ }
191+
192+ func (key * MasterKey ) decryptSymmetric (cloudkmsService * cloudkms.Service ) ([]byte , error ) {
78193 req := & cloudkms.DecryptRequest {
79194 Ciphertext : key .EncryptedKey ,
80195 }
@@ -92,6 +207,24 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
92207 return encryptedKey , nil
93208}
94209
210+ func (key * MasterKey ) decryptAsymmetric (cloudkmsService * cloudkms.Service ) ([]byte , error ) {
211+ req := & cloudkms.AsymmetricDecryptRequest {
212+ Ciphertext : key .EncryptedKey ,
213+ }
214+ resp , err := cloudkmsService .Projects .Locations .KeyRings .CryptoKeys .CryptoKeyVersions .AsymmetricDecrypt (key .ResourceID , req ).Do ()
215+ if err != nil {
216+ log .WithField ("resourceID" , key .ResourceID ).Info ("Asymmetric decryption failed" )
217+ return nil , fmt .Errorf ("Error decrypting key: %w" , err )
218+ }
219+ encryptedKey , err := base64 .StdEncoding .DecodeString (resp .Plaintext )
220+ if err != nil {
221+ log .WithField ("resourceID" , key .ResourceID ).Info ("Asymmetric decryption failed" )
222+ return nil , err
223+ }
224+ log .WithField ("resourceID" , key .ResourceID ).Info ("Asymmetric decryption succeeded" )
225+ return encryptedKey , nil
226+ }
227+
95228// NeedsRotation returns whether the data key needs to be rotated or not.
96229func (key * MasterKey ) NeedsRotation () bool {
97230 return time .Since (key .CreationDate ) > (time .Hour * 24 * 30 * 6 )
@@ -124,7 +257,7 @@ func MasterKeysFromResourceIDString(resourceID string) []*MasterKey {
124257}
125258
126259func (key MasterKey ) createCloudKMSService () (* cloudkms.Service , error ) {
127- re := regexp .MustCompile (`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$` )
260+ re := regexp .MustCompile (`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+(?:/cryptoKeyVersions/[^/]+)? $` )
128261 matches := re .FindStringSubmatch (key .ResourceID )
129262 if matches == nil {
130263 return nil , fmt .Errorf ("No valid resourceId found in %q" , key .ResourceID )
@@ -155,6 +288,18 @@ func (key MasterKey) ToMap() map[string]interface{} {
155288 return out
156289}
157290
291+ // assume key.ResourceID is in following format
292+ // - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key`
293+ // - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key/cryptoKeyVersions/version`
294+ func (key MasterKey ) resourceIDWithoutVersion () string {
295+ re := regexp .MustCompile (`^(projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+)(?:/cryptoKeyVersions/[^/]+)?$` )
296+ matches := re .FindStringSubmatch (key .ResourceID )
297+ if len (matches ) < 2 {
298+ return ""
299+ }
300+ return matches [1 ]
301+ }
302+
158303// getGoogleCredentials looks for a GCP Service Account in the environment
159304// variable: GOOGLE_CREDENTIALS, set as either a path to a credentials file or directly as the
160305// variable's value in JSON format.
@@ -167,3 +312,8 @@ func getGoogleCredentials() ([]byte, error) {
167312 }
168313 return []byte (defaultCredentials ), nil
169314}
315+
316+ func crc32c (data []byte ) uint32 {
317+ t := crc32 .MakeTable (crc32 .Castagnoli )
318+ return crc32 .Checksum (data , t )
319+ }
0 commit comments