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_AUTO_ROTATION = "enable.key.auto.rotation" ;
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 enableKeyAutoRotation ;
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,8 @@ 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 .enableKeyAutoRotation = Boolean .getBoolean (properties .getOrDefault (ENABLE_KEY_AUTO_ROTATION , "true" ));
96+
9097 }
9198
9299 /**
@@ -115,7 +122,7 @@ private String getSystemProjectId(String metadataServerApi) throws IOException {
115122 * @return an authorized CloudKMS client
116123 * @throws IOException if credentials can not be created in current environment
117124 */
118- private CloudKMS createCloudKMS (String serviceAccountFile ) throws IOException {
125+ protected CloudKMS createCloudKMS (String serviceAccountFile ) throws IOException {
119126 HttpTransport transport = new NetHttpTransport ();
120127 JsonFactory jsonFactory = new JacksonFactory ();
121128 GoogleCredential credential ;
@@ -163,13 +170,78 @@ void createKeyRingIfNotExists() throws IOException {
163170 }
164171 }
165172
173+ public void enableAutoRotationForExistingKeys () throws IOException {
174+ if (!this .enableKeyAutoRotation ){
175+ return ;
176+ }
177+ String parentKeyRing = String .format ("projects/%s/locations/%s/keyRings/%s" , projectId ,
178+ LOCATION_ID , keyringId );
179+ LOG .debug ("Listing crypto keys in keyring {}." , keyringId );
180+ try {
181+ List <CryptoKey > cryptoKeys = cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
182+ .list (parentKeyRing ).execute ().getCryptoKeys ();
183+ if (cryptoKeys == null || cryptoKeys .isEmpty ()) {
184+ LOG .info ("No existing crypto keys found in keyring {}." , keyringId );
185+ return ;
186+ }
187+ LOG .info ("Found {} existing crypto keys in keyring {}. Checking rotation periods." ,
188+ cryptoKeys .size (), keyringId );
189+
190+ for (CryptoKey cryptoKey : cryptoKeys ) {
191+ String cryptoKeyName = cryptoKey .getName (); // Full resource name of the crypto key
192+ LOG .debug ("Full crypto key name: {}" , cryptoKeyName );
193+ String cryptoKeyId = cryptoKeyName .substring (cryptoKeyName .lastIndexOf ('/' ) + 1 );
194+ LOG .debug ("crypto key id: {}" , cryptoKeyId );
195+ if (cryptoKey .getRotationPeriod () == null || cryptoKey .getRotationPeriod ().isEmpty ()) {
196+ LOG .info ("Crypto key {} does not have a rotation period set. Updating it." , cryptoKeyId );
197+
198+ // Create an update request
199+ CryptoKey updateRequest = new CryptoKey ();
200+ long rotationInSeconds = java .time .Duration .ofDays (1 ).getSeconds ();
201+ updateRequest .setRotationPeriod (rotationInSeconds + "s" );
202+
203+ // Calculate next rotation time (1 minute from now), FOR PRODUCTION, MAKE THIS A REASONABLE FUTURE DATE.
204+ Instant nextRotationTime = Instant .now ().plus (java .time .Duration .ofMinutes (1 ));
205+ updateRequest .setNextRotationTime (nextRotationTime .toString ());
206+
207+ // The resource name for the update operation is the full name of the cryptokey
208+ String resourceName = String .format ("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s" ,
209+ projectId , LOCATION_ID , keyringId , cryptoKeyId );
210+
211+ LOG .debug ("Key resource: {}, updates rotation period: {}, updates rotation time next: {}" , resourceName , rotationInSeconds +"s" ,nextRotationTime .toString ());
212+
213+ try {
214+ // Patch the crypto key to update its rotation period and next rotation time
215+ cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
216+ .patch (resourceName , updateRequest )
217+ .setUpdateMask (
218+ "rotation_period,next_rotation_time" ) // IMPORTANT: specify fields to update
219+ .execute ();
220+ LOG .info ("Successfully updated rotation period for crypto key {}." , cryptoKeyId );
221+ } catch (GoogleJsonResponseException e ) {
222+ LOG .error ("Failed to update rotation period for crypto key {}: {}" , cryptoKeyId ,
223+ e .getMessage (), e );
224+ // Decide if you want to throw an exception here or just log and continue
225+ }
226+ } else {
227+ LOG .debug ("Crypto key {} already has a rotation period set ({}). Skipping." ,
228+ cryptoKeyId , cryptoKey .getRotationPeriod ());
229+ }
230+ }
231+
232+ } catch (GoogleJsonResponseException e ) {
233+ throw new IOException (
234+ String .format ("Exception occurred while listing crypto keys in keyring %s: %s" , keyringId , e .getMessage ()), e );
235+ }
236+ }
237+
166238 /**
167239 * Creates a new crypto key on google cloud kms with the given id.
168240 *
169241 * @param cryptoKeyId crypto key id
170242 * @throws IOException if there's an error creating crypto key
171243 */
172- void createCryptoKeyIfNotExists (String cryptoKeyId ) throws IOException {
244+ public void createCryptoKeyIfNotExists (String cryptoKeyId ) throws IOException {
173245 // If crypto key is already created, do not attempt to create it again.
174246 if (knownCryptoKeys .contains (cryptoKeyId )) {
175247 return ;
@@ -181,6 +253,14 @@ void createCryptoKeyIfNotExists(String cryptoKeyId) throws IOException {
181253 CryptoKey cryptoKey = new CryptoKey ();
182254 // This will allow the API access to the key for symmetric encryption and decryption.
183255 cryptoKey .setPurpose (ENCRYPT_DECRYPT );
256+ if (this .enableKeyAutoRotation ) {
257+ // long rotationInSeconds = java.time.Duration.ofDays(90).getSeconds();
258+ long rotationInSeconds = java .time .Duration .ofDays (1 ).getSeconds ();
259+ cryptoKey .setRotationPeriod (rotationInSeconds + "s" );
260+ // Calculate next rotation time (1 minute from now), FOR TESTING ONLY.
261+ Instant nextRotationTime = Instant .now ().plus (java .time .Duration .ofMinutes (1 ));
262+ cryptoKey .setNextRotationTime (nextRotationTime .toString ());
263+ }
184264
185265 try {
186266 cloudKMS .projects ().locations ().keyRings ().cryptoKeys ()
0 commit comments