4040import java .net .HttpURLConnection ;
4141import java .net .URL ;
4242import java .nio .charset .StandardCharsets ;
43+ import java .time .Instant ;
4344import java .util .HashSet ;
45+ import java .util .List ;
4446import java .util .Map ;
4547import java .util .Set ;
4648import org .slf4j .Logger ;
4951/**
5052 * Wrapper on {@link CloudKMS} client.
5153 */
52- class CloudKMSClient implements Closeable {
54+ public class CloudKMSClient implements Closeable {
5355
5456 private static final Logger LOG = LoggerFactory .getLogger (CloudKMSClient .class );
5557 // When created in the global location, Cloud KMS resources are available from zones spread around the world.
@@ -59,6 +61,7 @@ class CloudKMSClient implements Closeable {
5961 private static final String CLOUD_KMS = "cloudkms" ;
6062 private static final String PROJECT_ID = "project.id" ;
6163 private static final String SERVICE_ACCOUNT_FILE = "service.account.file" ;
64+ private static final String ENABLE_KEY_ROTATION = "key.rotation.enabled" ;
6265 private static final String METADATA_SERVER_API = "metadata.server.api" ;
6366 private static final String DEFAULT_METADATA_SERVER_API =
6467 "http://metadata.google.internal/computeMetadata"
@@ -73,12 +76,14 @@ class CloudKMSClient implements Closeable {
7376 // In-memory cache to hold created crypto keys, this is to avoid checking if a given crypto key exists.
7477 private final Set <String > knownCryptoKeys ;
7578
79+ private final Boolean enableKeyRotation ;
80+
7681 /**
7782 * Constructs Cloud KMS client.
7883 *
7984 * @throws IOException if cloud kms client can not be created
8085 */
81- CloudKMSClient (Map <String , String > properties ) throws IOException {
86+ protected CloudKMSClient (Map <String , String > properties ) throws IOException {
8287 String metadataServerApi = properties .getOrDefault (METADATA_SERVER_API ,
8388 DEFAULT_METADATA_SERVER_API );
8489 this .projectId = properties .containsKey (PROJECT_ID ) ? properties .get (PROJECT_ID ) :
@@ -87,6 +92,9 @@ class CloudKMSClient implements Closeable {
8792 this .cloudKMS = createCloudKMS (serviceAccountFile );
8893 this .keyringId = properties .getOrDefault (KEYRING_ID , DEFAULT_KEYRING_ID );
8994 this .knownCryptoKeys = new HashSet <>();
95+ this .enableKeyRotation = Boolean .parseBoolean (
96+ properties .getOrDefault (ENABLE_KEY_ROTATION , "true" ));
97+
9098 }
9199
92100 /**
@@ -115,7 +123,7 @@ private String getSystemProjectId(String metadataServerApi) throws IOException {
115123 * @return an authorized CloudKMS client
116124 * @throws IOException if credentials can not be created in current environment
117125 */
118- private CloudKMS createCloudKMS (String serviceAccountFile ) throws IOException {
126+ protected CloudKMS createCloudKMS (String serviceAccountFile ) throws IOException {
119127 HttpTransport transport = new NetHttpTransport ();
120128 JsonFactory jsonFactory = new JacksonFactory ();
121129 GoogleCredential credential ;
@@ -163,13 +171,66 @@ void createKeyRingIfNotExists() throws IOException {
163171 }
164172 }
165173
174+ public void enableRotationForExistingKeys () throws IOException {
175+ if (!this .enableKeyRotation ) {
176+ return ;
177+ }
178+
179+ String parentKeyRing = String .format ("projects/%s/locations/%s/keyRings/%s" , projectId ,
180+ LOCATION_ID , keyringId );
181+ try {
182+ List <CryptoKey > cryptoKeys = cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
183+ .list (parentKeyRing ).execute ().getCryptoKeys ();
184+ if (cryptoKeys == null || cryptoKeys .isEmpty ()) {
185+ LOG .info ("No existing crypto keys found in keyring {}." , keyringId );
186+ return ;
187+ }
188+
189+ for (CryptoKey cryptoKey : cryptoKeys ) {
190+ String cryptoKeyName = cryptoKey .getName ();
191+ String cryptoKeyId = cryptoKeyName .substring (
192+ cryptoKeyName .lastIndexOf ('/' ) + 1 );
193+
194+ if (cryptoKey .getRotationPeriod () == null || cryptoKey .getRotationPeriod ().isEmpty ()) {
195+ CryptoKey updateRequest = new CryptoKey ();
196+ // Setting rotation period to 90 days for KMS keys.
197+ long rotationInSeconds = java .time .Duration .ofDays (90 ).getSeconds ();
198+ updateRequest .setRotationPeriod (rotationInSeconds + "s" );
199+ // Setting nextRotationTime to 1 day after the key is updated.
200+ Instant nextRotationTime = Instant .now ().plus (java .time .Duration .ofDays (1 ));
201+ updateRequest .setNextRotationTime (nextRotationTime .toString ());
202+
203+ String resourceName = String .format ("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s" ,
204+ projectId , LOCATION_ID , keyringId , cryptoKeyId );
205+
206+ try {
207+ cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
208+ .patch (resourceName , updateRequest )
209+ .setUpdateMask (
210+ "rotation_period,next_rotation_time" )
211+ .execute ();
212+ LOG .info ("Successfully updated rotation period for crypto key {}." , cryptoKeyId );
213+ } catch (GoogleJsonResponseException e ) {
214+ throw new IOException (
215+ String .format ("Failed to update rotation period for crypto key %s due to %s" ,
216+ cryptoKeyId , e .getMessage ()), e );
217+ }
218+ }
219+ }
220+ } catch (GoogleJsonResponseException e ) {
221+ throw new IOException (
222+ String .format ("Exception occurred while listing crypto keys in keyring %s: %s" ,
223+ keyringId , e .getMessage ()), e );
224+ }
225+ }
226+
166227 /**
167228 * Creates a new crypto key on google cloud kms with the given id.
168229 *
169230 * @param cryptoKeyId crypto key id
170231 * @throws IOException if there's an error creating crypto key
171232 */
172- void createCryptoKeyIfNotExists (String cryptoKeyId ) throws IOException {
233+ public void createCryptoKeyIfNotExists (String cryptoKeyId ) throws IOException {
173234 // If crypto key is already created, do not attempt to create it again.
174235 if (knownCryptoKeys .contains (cryptoKeyId )) {
175236 return ;
@@ -182,6 +243,14 @@ void createCryptoKeyIfNotExists(String cryptoKeyId) throws IOException {
182243 // This will allow the API access to the key for symmetric encryption and decryption.
183244 cryptoKey .setPurpose (ENCRYPT_DECRYPT );
184245
246+ if (this .enableKeyRotation ) {
247+ long rotationInSeconds = java .time .Duration .ofDays (90 ).getSeconds ();
248+ cryptoKey .setRotationPeriod (rotationInSeconds + "s" );
249+
250+ Instant nextRotationTime = Instant .now ().plus (java .time .Duration .ofDays (1 ));
251+ cryptoKey .setNextRotationTime (nextRotationTime .toString ());
252+ }
253+
185254 try {
186255 cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
187256 .create (parent , cryptoKey )
0 commit comments