3030import com .google .api .services .cloudkms .v1 .model .EncryptRequest ;
3131import com .google .api .services .cloudkms .v1 .model .EncryptResponse ;
3232import com .google .api .services .cloudkms .v1 .model .KeyRing ;
33+ import com .google .common .annotations .VisibleForTesting ;
3334import com .google .common .io .CharStreams ;
3435import java .io .Closeable ;
3536import java .io .File ;
4041import java .net .HttpURLConnection ;
4142import java .net .URL ;
4243import java .nio .charset .StandardCharsets ;
44+ import java .time .Instant ;
4345import java .util .HashSet ;
46+ import java .util .List ;
4447import java .util .Map ;
4548import java .util .Set ;
4649import org .slf4j .Logger ;
@@ -59,19 +62,23 @@ class CloudKMSClient implements Closeable {
5962 private static final String CLOUD_KMS = "cloudkms" ;
6063 private static final String PROJECT_ID = "project.id" ;
6164 private static final String SERVICE_ACCOUNT_FILE = "service.account.file" ;
65+ private static final String ENABLE_KEY_ROTATION = "key.rotation.enabled" ;
6266 private static final String METADATA_SERVER_API = "metadata.server.api" ;
6367 private static final String DEFAULT_METADATA_SERVER_API =
6468 "http://metadata.google.internal/computeMetadata"
6569 + "/v1/project/project-id" ;
6670 private static final String KEYRING_ID = "keyring.id" ;
6771 private static final String DEFAULT_KEYRING_ID = "cdap" ;
72+ private static final String KEY_RING_FORMAT = "projects/%s/locations/%s/keyRings/%s" ;
73+ private static final String CRYPTO_KEY_FORMAT = KEY_RING_FORMAT + "/cryptoKeys/%s" ;
6874
6975
7076 private final CloudKMS cloudKMS ;
7177 private final String projectId ;
7278 private final String keyringId ;
7379 // In-memory cache to hold created crypto keys, this is to avoid checking if a given crypto key exists.
7480 private final Set <String > knownCryptoKeys ;
81+ private final Boolean enableKeyRotation ;
7582
7683 /**
7784 * Constructs Cloud KMS client.
@@ -87,6 +94,8 @@ class CloudKMSClient implements Closeable {
8794 this .cloudKMS = createCloudKMS (serviceAccountFile );
8895 this .keyringId = properties .getOrDefault (KEYRING_ID , DEFAULT_KEYRING_ID );
8996 this .knownCryptoKeys = new HashSet <>();
97+ this .enableKeyRotation = Boolean .parseBoolean (
98+ properties .getOrDefault (ENABLE_KEY_ROTATION , "true" ));
9099 }
91100
92101 /**
@@ -115,7 +124,8 @@ private String getSystemProjectId(String metadataServerApi) throws IOException {
115124 * @return an authorized CloudKMS client
116125 * @throws IOException if credentials can not be created in current environment
117126 */
118- private CloudKMS createCloudKMS (String serviceAccountFile ) throws IOException {
127+ @ VisibleForTesting
128+ CloudKMS createCloudKMS (String serviceAccountFile ) throws IOException {
119129 HttpTransport transport = new NetHttpTransport ();
120130 JsonFactory jsonFactory = new JacksonFactory ();
121131 GoogleCredential credential ;
@@ -163,6 +173,56 @@ void createKeyRingIfNotExists() throws IOException {
163173 }
164174 }
165175
176+ void enableRotationForExistingKeys () throws IOException {
177+ if (!this .enableKeyRotation ) {
178+ return ;
179+ }
180+
181+ String parentKeyRing = String .format (KEY_RING_FORMAT , projectId , LOCATION_ID , keyringId );
182+ try {
183+ List <CryptoKey > cryptoKeys = cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
184+ .list (parentKeyRing ).execute ().getCryptoKeys ();
185+ if (cryptoKeys == null || cryptoKeys .isEmpty ()) {
186+ LOG .info ("No existing crypto keys found in keyring {}." , keyringId );
187+ return ;
188+ }
189+
190+ for (CryptoKey cryptoKey : cryptoKeys ) {
191+ String cryptoKeyName = cryptoKey .getName ();
192+ String cryptoKeyId = cryptoKeyName .substring (
193+ cryptoKeyName .lastIndexOf ('/' ) + 1 );
194+
195+ if (cryptoKey .getRotationPeriod () == null || cryptoKey .getRotationPeriod ().isEmpty ()) {
196+ CryptoKey updateRequest = new CryptoKey ();
197+ // Setting rotation period to 90 days for KMS keys.
198+ long rotationInSeconds = java .time .Duration .ofDays (90 ).getSeconds ();
199+ updateRequest .setRotationPeriod (rotationInSeconds + "s" );
200+ // Setting nextRotationTime to 1 day after the key is updated.
201+ Instant nextRotationTime = Instant .now ().plus (java .time .Duration .ofDays (1 ));
202+ updateRequest .setNextRotationTime (nextRotationTime .toString ());
203+
204+ String resourceName = String .format (CRYPTO_KEY_FORMAT , projectId , LOCATION_ID , keyringId , cryptoKeyId );
205+ try {
206+ cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
207+ .patch (resourceName , updateRequest )
208+ .setUpdateMask (
209+ "rotation_period,next_rotation_time" )
210+ .execute ();
211+ LOG .info ("Successfully updated rotation period for crypto key {}." , cryptoKeyId );
212+ } catch (GoogleJsonResponseException e ) {
213+ throw new IOException (
214+ String .format ("Failed to update rotation period for crypto key %s due to %s" ,
215+ cryptoKeyId , e .getMessage ()), e );
216+ }
217+ }
218+ }
219+ } catch (GoogleJsonResponseException e ) {
220+ throw new IOException (
221+ String .format ("Exception occurred while listing crypto keys in keyring %s: %s" ,
222+ keyringId , e .getMessage ()), e );
223+ }
224+ }
225+
166226 /**
167227 * Creates a new crypto key on google cloud kms with the given id.
168228 *
@@ -175,13 +235,20 @@ void createCryptoKeyIfNotExists(String cryptoKeyId) throws IOException {
175235 return ;
176236 }
177237
178- String parent = String .format ("projects/%s/locations/%s/keyRings/%s" , projectId , LOCATION_ID ,
179- keyringId );
238+ String parent = String .format (KEY_RING_FORMAT , projectId , LOCATION_ID , keyringId );
180239
181240 CryptoKey cryptoKey = new CryptoKey ();
182241 // This will allow the API access to the key for symmetric encryption and decryption.
183242 cryptoKey .setPurpose (ENCRYPT_DECRYPT );
184243
244+ if (this .enableKeyRotation ) {
245+ long rotationInSeconds = java .time .Duration .ofDays (90 ).getSeconds ();
246+ cryptoKey .setRotationPeriod (rotationInSeconds + "s" );
247+
248+ Instant nextRotationTime = Instant .now ().plus (java .time .Duration .ofDays (1 ));
249+ cryptoKey .setNextRotationTime (nextRotationTime .toString ());
250+ }
251+
185252 try {
186253 cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
187254 .create (parent , cryptoKey )
@@ -210,8 +277,7 @@ void createCryptoKeyIfNotExists(String cryptoKeyId) throws IOException {
210277 * @throws IOException there's an error in encrypting secret
211278 */
212279 byte [] encrypt (String cryptoKeyId , byte [] secret ) throws IOException {
213- String resourceName = String .format ("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s" ,
214- projectId , LOCATION_ID ,
280+ String resourceName = String .format (CRYPTO_KEY_FORMAT , projectId , LOCATION_ID ,
215281 keyringId , cryptoKeyId );
216282 // secret must not be longer than 64KiB.
217283 EncryptRequest request = new EncryptRequest ().encodePlaintext (secret );
@@ -236,8 +302,8 @@ byte[] encrypt(String cryptoKeyId, byte[] secret) throws IOException {
236302 * @throws IOException there's an error in decrypting secret
237303 */
238304 byte [] decrypt (String cryptoKeyId , byte [] encryptedSecret ) throws IOException {
239- String resourceName = String .format ("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s" ,
240- projectId , LOCATION_ID , keyringId , cryptoKeyId );
305+ String resourceName = String .format (CRYPTO_KEY_FORMAT , projectId , LOCATION_ID , keyringId ,
306+ cryptoKeyId );
241307
242308 DecryptRequest request = new DecryptRequest ().encodeCiphertext (encryptedSecret );
243309 DecryptResponse response = cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
0 commit comments