diff --git a/gradle/check.gradle b/gradle/check.gradle index c67247675..7bd048de1 100644 --- a/gradle/check.gradle +++ b/gradle/check.gradle @@ -42,6 +42,10 @@ jacocoTestReport { } } +dependencyCheck { + failOnError = false +} + // To generate an HTML report instead of XML tasks.withType(SpotBugsTask) { reports.xml.enabled = false diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index c6f080326..5b0b34a8e 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -28,7 +28,7 @@ def logbackVersion = '1.1.7' // Request version 4 for full java 8 support def guiceVersion = '4.2.2' def awsSdkVersion = '1.11.160' -def groovyVersion = '2.3.9' +def groovyVersion = '2.5.7' dependencies { compile ( diff --git a/src/main/java/com/nike/cerberus/dao/SecureDataDao.java b/src/main/java/com/nike/cerberus/dao/SecureDataDao.java index e1c149714..796744b33 100644 --- a/src/main/java/com/nike/cerberus/dao/SecureDataDao.java +++ b/src/main/java/com/nike/cerberus/dao/SecureDataDao.java @@ -18,6 +18,7 @@ import com.nike.cerberus.domain.SecureDataType; import com.nike.cerberus.mapper.SecureDataMapper; +import com.nike.cerberus.record.DataKeyInfo; import com.nike.cerberus.record.SecureDataRecord; import javax.inject.Inject; @@ -54,6 +55,7 @@ public void writeSecureData(String sdbId, String path, byte[] encryptedPayload, .setCreatedTs(createdTs) .setLastUpdatedBy(lastUpdatedBy) .setLastUpdatedTs(lastUpdatedTs) + .setLastRotatedTs(lastUpdatedTs) // This is intentional ); } @@ -66,7 +68,8 @@ public void updateSecureData(String sdbId, String createdBy, OffsetDateTime createdTs, String lastUpdatedBy, - OffsetDateTime lastUpdatedTs) { + OffsetDateTime lastUpdatedTs, + OffsetDateTime lastRotatedTs) { secureDataMapper.updateSecureData(new SecureDataRecord() .setId(path.hashCode()) @@ -80,14 +83,23 @@ public void updateSecureData(String sdbId, .setCreatedTs(createdTs) .setLastUpdatedTs(lastUpdatedTs) .setLastUpdatedBy(lastUpdatedBy) + .setLastRotatedTs(lastRotatedTs) ); } + public int updateSecureData(SecureDataRecord secureDataRecord) { + return secureDataMapper.updateSecureData(secureDataRecord); + } + public Optional readSecureDataByPath(String sdbId, String path) { return Optional.ofNullable(secureDataMapper.readSecureDataByPath(sdbId, path)); } + public Optional readSecureDataByIdLocking(String id) { + return Optional.ofNullable(secureDataMapper.readSecureDataByIdLocking(id)); + } + public Optional readSecureDataByPathAndType(String sdbId, String path, SecureDataType type) { return Optional.ofNullable(secureDataMapper.readSecureDataByPathAndType(sdbId, path, type)); } @@ -136,4 +148,8 @@ public int getSumTopLevelKeyValuePairs() { Integer val = secureDataMapper.getSumTopLevelKeyValuePairs(); return val == null ? 0 : val; } + + public List getOldestDataKeyInfo(OffsetDateTime dateTime, int limit) { + return secureDataMapper.getOldestDataKeyInfo(dateTime, limit); + } } diff --git a/src/main/java/com/nike/cerberus/dao/SecureDataVersionDao.java b/src/main/java/com/nike/cerberus/dao/SecureDataVersionDao.java index 920cfb578..a3e56a9be 100644 --- a/src/main/java/com/nike/cerberus/dao/SecureDataVersionDao.java +++ b/src/main/java/com/nike/cerberus/dao/SecureDataVersionDao.java @@ -66,6 +66,10 @@ public void writeSecureDataVersion(String sdbId, ); } + public int updateSecureDataVersion(SecureDataVersionRecord secureDataVersionRecord) { + return secureDataVersionMapper.updateSecureDataVersion(secureDataVersionRecord); + } + public Integer getTotalNumVersionsForPath(String path) { return secureDataVersionMapper.getTotalNumVersionsForPath(path); } @@ -78,6 +82,10 @@ public Optional readSecureDataVersionById(String id) { return Optional.ofNullable(secureDataVersionMapper.readSecureDataVersionById(id)); } + public Optional readSecureDataVersionByIdLocking(String id) { + return Optional.ofNullable(secureDataVersionMapper.readSecureDataVersionByIdLocking(id)); + } + public String[] getVersionPathsByPartialPath(String partialPath) { return secureDataVersionMapper.getVersionPathsByPartialPath(partialPath); } diff --git a/src/main/java/com/nike/cerberus/domain/Source.java b/src/main/java/com/nike/cerberus/domain/Source.java new file mode 100644 index 000000000..651c3b49a --- /dev/null +++ b/src/main/java/com/nike/cerberus/domain/Source.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.domain; + +/** + * Enum used to distinguish between objects, files, and other types of secrets in the database. + */ +public enum Source { + SECURE_DATA, + SECURE_DATA_VERSION +} diff --git a/src/main/java/com/nike/cerberus/jobs/DataKeyRotationJob.java b/src/main/java/com/nike/cerberus/jobs/DataKeyRotationJob.java new file mode 100644 index 000000000..871c37927 --- /dev/null +++ b/src/main/java/com/nike/cerberus/jobs/DataKeyRotationJob.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.jobs; + + +import com.nike.cerberus.service.SecureDataService; + +import javax.inject.Inject; +import javax.inject.Named; + +public class DataKeyRotationJob extends LockingJob { + + private final SecureDataService secureDataService; + private final int numberOfDataKeyToRotatePerJobRun; + private final int dataKeyRotationPauseTimeInMillis; + private final int dataKeyRotationIntervalInDays; + + @Inject + public DataKeyRotationJob(SecureDataService secureDataService, + @Named("cms.jobs.DataKeyRotationJob.numberOfDataKeyToRotatePerJobRun") + int numberOfDataKeyToRotatePerJobRun, + @Named("cms.jobs.DataKeyRotationJob.dataKeyRotationPauseTimeInMillis") + int dataKeyRotationPauseTimeInMillis, + @Named("cms.jobs.DataKeyRotationJob.dataKeyRotationIntervalInDays") + int dataKeyRotationIntervalInDays) { + + this.secureDataService = secureDataService; + this.numberOfDataKeyToRotatePerJobRun = numberOfDataKeyToRotatePerJobRun; + this.dataKeyRotationPauseTimeInMillis = dataKeyRotationPauseTimeInMillis; + this.dataKeyRotationIntervalInDays = dataKeyRotationIntervalInDays; + } + + + @Override + protected void executeLockableCode() { + secureDataService.rotateDataKeys(numberOfDataKeyToRotatePerJobRun, dataKeyRotationPauseTimeInMillis, dataKeyRotationIntervalInDays); + } +} diff --git a/src/main/java/com/nike/cerberus/mapper/SecureDataMapper.java b/src/main/java/com/nike/cerberus/mapper/SecureDataMapper.java index 3de62e0e3..086bef126 100644 --- a/src/main/java/com/nike/cerberus/mapper/SecureDataMapper.java +++ b/src/main/java/com/nike/cerberus/mapper/SecureDataMapper.java @@ -17,9 +17,11 @@ package com.nike.cerberus.mapper; import com.nike.cerberus.domain.SecureDataType; +import com.nike.cerberus.record.DataKeyInfo; import com.nike.cerberus.record.SecureDataRecord; import org.apache.ibatis.annotations.Param; +import java.time.OffsetDateTime; import java.util.List; import java.util.Set; @@ -31,6 +33,8 @@ public interface SecureDataMapper { SecureDataRecord readSecureDataByPath(@Param("sdbId") String sdbId, @Param("path") String path); + SecureDataRecord readSecureDataByIdLocking(@Param("id") String id); + SecureDataRecord readSecureDataByPathAndType(@Param("sdbId") String sdbId, @Param("path") String path, @Param("type") SecureDataType type); SecureDataRecord readMetadataByPathAndType(@Param("sdbId") String sdbId, @Param("path") String path, @Param("type") SecureDataType type); @@ -60,4 +64,7 @@ int deleteAllSecretsThatStartWithGivenPartialPath(@Param("sdbId") String sdbId, int deleteSecret(@Param("sdbId") String sdbId, @Param("path") String path); Integer getSumTopLevelKeyValuePairs(); + + List getOldestDataKeyInfo(@Param("datetime") OffsetDateTime dateTime, + @Param("limit") int limit); } diff --git a/src/main/java/com/nike/cerberus/mapper/SecureDataVersionMapper.java b/src/main/java/com/nike/cerberus/mapper/SecureDataVersionMapper.java index 3eb251efa..c856eb9b2 100644 --- a/src/main/java/com/nike/cerberus/mapper/SecureDataVersionMapper.java +++ b/src/main/java/com/nike/cerberus/mapper/SecureDataVersionMapper.java @@ -20,6 +20,7 @@ import com.nike.cerberus.record.SecureDataVersionRecord; import org.apache.ibatis.annotations.Param; +import java.time.OffsetDateTime; import java.util.List; import java.util.Set; @@ -27,6 +28,8 @@ public interface SecureDataVersionMapper { int writeSecureDataVersion(@Param("record") SecureDataVersionRecord record); + int updateSecureDataVersion(@Param("record") SecureDataVersionRecord record); + Integer getTotalNumVersionsForPath(String path); List listSecureDataVersionsByPath(@Param("path") String path, @@ -35,6 +38,8 @@ List listSecureDataVersionsByPath(@Param("path") String SecureDataVersionRecord readSecureDataVersionById(@Param("id") String id); + SecureDataVersionRecord readSecureDataVersionByIdLocking(@Param("id") String id); + String[] getVersionPathsByPartialPath(@Param("partialPath") String partialPath); Set getVersionPathsBySdbId(@Param("sdbId") String sdbId); diff --git a/src/main/java/com/nike/cerberus/record/DataKeyInfo.java b/src/main/java/com/nike/cerberus/record/DataKeyInfo.java new file mode 100644 index 000000000..519924f85 --- /dev/null +++ b/src/main/java/com/nike/cerberus/record/DataKeyInfo.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.record; + +import com.nike.cerberus.domain.Source; + +import java.time.OffsetDateTime; + +public class DataKeyInfo { + + private String id; + private Source source; + private OffsetDateTime lastRotatedTs; + + public String getId() { + return id; + } + + public DataKeyInfo setId(String id) { + this.id = id; + return this; + } + + public Source getSource() { + return source; + } + + public DataKeyInfo setSource(Source source) { + this.source = source; + return this; + } + + public OffsetDateTime getLastRotatedTs() { + return lastRotatedTs; + } + + public DataKeyInfo setLastRotatedTs(OffsetDateTime lastRotatedTs) { + this.lastRotatedTs = lastRotatedTs; + return this; + } +} diff --git a/src/main/java/com/nike/cerberus/record/SecureDataRecord.java b/src/main/java/com/nike/cerberus/record/SecureDataRecord.java index 3e111964e..5c9d1d5d9 100644 --- a/src/main/java/com/nike/cerberus/record/SecureDataRecord.java +++ b/src/main/java/com/nike/cerberus/record/SecureDataRecord.java @@ -34,6 +34,7 @@ public class SecureDataRecord { private String createdBy; private OffsetDateTime lastUpdatedTs; private String lastUpdatedBy; + private OffsetDateTime lastRotatedTs; public Integer getId() { return id; @@ -137,4 +138,13 @@ public SecureDataRecord setLastUpdatedBy(String lastUpdatedBy) { this.lastUpdatedBy = lastUpdatedBy; return this; } + + public OffsetDateTime getLastRotatedTs() { + return lastRotatedTs; + } + + public SecureDataRecord setLastRotatedTs(OffsetDateTime lastRotatedTs) { + this.lastRotatedTs = lastRotatedTs; + return this; + } } diff --git a/src/main/java/com/nike/cerberus/record/SecureDataVersionRecord.java b/src/main/java/com/nike/cerberus/record/SecureDataVersionRecord.java index 3a0722456..a6f7604a9 100644 --- a/src/main/java/com/nike/cerberus/record/SecureDataVersionRecord.java +++ b/src/main/java/com/nike/cerberus/record/SecureDataVersionRecord.java @@ -35,6 +35,7 @@ public class SecureDataVersionRecord { private OffsetDateTime versionCreatedTs; private String actionPrincipal; private OffsetDateTime actionTs; + private OffsetDateTime lastRotatedTs; public String getId() { return id; @@ -144,4 +145,13 @@ public enum SecretsAction { UPDATE, DELETE } + + public OffsetDateTime getLastRotatedTs() { + return lastRotatedTs; + } + + public SecureDataVersionRecord setLastRotatedTs(OffsetDateTime lastRotatedTs) { + this.lastRotatedTs = lastRotatedTs; + return this; + } } diff --git a/src/main/java/com/nike/cerberus/service/EncryptionService.java b/src/main/java/com/nike/cerberus/service/EncryptionService.java index 90ce25048..ccbe9b7ad 100644 --- a/src/main/java/com/nike/cerberus/service/EncryptionService.java +++ b/src/main/java/com/nike/cerberus/service/EncryptionService.java @@ -122,6 +122,28 @@ private String decrypt(ParsedCiphertext parsedCiphertext, String sdbPath) { return new String(awsCrypto.decryptData(decryptProvider, parsedCiphertext).getResult(), StandardCharsets.UTF_8); } + /** + * Re-encrypt (i.e. decrypt then encrypt) String ciphertext + * @param encryptedPayload encryptedPayload + * @param sdbPath the current SDB path + * @return re-encrypted ciphertext + */ + public String reencrypt(String encryptedPayload, String sdbPath) { + String plaintext = decrypt(encryptedPayload, sdbPath); + return encrypt(plaintext, sdbPath); + } + + /** + * Re-encrypt (i.e. decrypt then encrypt) byte array ciphertext + * @param encryptedPayload encryptedPayload + * @param sdbPath the current SDB path + * @return re-encrypted ciphertext + */ + public byte[] reencrypt(byte[] encryptedPayload, String sdbPath) { + byte[] plaintextBytes = decrypt(encryptedPayload, sdbPath); + return encrypt(plaintextBytes, sdbPath); + } + /** * Decrypt the encryptedPayload. * diff --git a/src/main/java/com/nike/cerberus/service/SecureDataService.java b/src/main/java/com/nike/cerberus/service/SecureDataService.java index 1889206e9..9bc71b07a 100644 --- a/src/main/java/com/nike/cerberus/service/SecureDataService.java +++ b/src/main/java/com/nike/cerberus/service/SecureDataService.java @@ -30,7 +30,9 @@ import com.nike.cerberus.domain.SecureFile; import com.nike.cerberus.domain.SecureFileSummary; import com.nike.cerberus.domain.SecureFileSummaryResult; +import com.nike.cerberus.domain.Source; import com.nike.cerberus.error.DefaultApiError; +import com.nike.cerberus.record.DataKeyInfo; import com.nike.cerberus.record.SecureDataRecord; import com.nike.cerberus.record.SecureDataVersionRecord; import com.nike.cerberus.util.DateTimeSupplier; @@ -111,7 +113,8 @@ public void writeSecret(String sdbId, String path, String plainTextPayload, Stri secureData.getCreatedBy(), secureData.getCreatedTs(), principal, - now); + now, + secureData.getLastRotatedTs()); } else { secureDataDao.writeSecureData(sdbId, path, ciphertextBytes, topLevelKVPairCount, SecureDataType.OBJECT, @@ -157,7 +160,8 @@ public void writeSecureFile(String sdbId, String path, byte[] bytes, int sizeInB secureData.getCreatedBy(), secureData.getCreatedTs(), principal, - now); + now, + secureData.getLastRotatedTs()); } else { secureDataDao.writeSecureData(sdbId, path, ciphertextBytes, topLevelKVPairCount, SecureDataType.FILE, @@ -444,4 +448,102 @@ public Map parseSecretMetadata(SecureData secureData) { public int getTotalNumberOfFiles() { return secureDataDao.countByType(SecureDataType.FILE); } + + /** + * Rotate data keys by re-encrypting data. The oldest data keys in both secure data and secure data version that are + * expired (based on rotation interval) will be considered for key rotation. + * @param numberOfKeys Max number of data keys to be rotated + * @param pauseTimeInMillis Number of millisecond to pause between re-encryption operations + * @param rotationIntervalInDays Data keys generated older than X days will be considered for key rotation + */ + public void rotateDataKeys(int numberOfKeys, int pauseTimeInMillis, int rotationIntervalInDays) { + int[] counter = new int[2]; + OffsetDateTime now = dateTimeSupplier.get(); + OffsetDateTime expiredTs = now.minusDays(rotationIntervalInDays); + List oldestDataKeyInfos = secureDataDao.getOldestDataKeyInfo(expiredTs, numberOfKeys); + + for (DataKeyInfo dataKeyInfo: oldestDataKeyInfos) { + Source source = dataKeyInfo.getSource(); + if (source == Source.SECURE_DATA) { + String id = dataKeyInfo.getId(); + try { + reencryptData(id); + counter[0]++; + } catch (Exception e) { + log.error("Failed to re-encrypt secure data id: {}", id); + } + } else if (source == Source.SECURE_DATA_VERSION) { + String versionId = dataKeyInfo.getId(); + try { + reencryptDataVersion(versionId); + counter[1]++; + } catch (Exception e) { + log.error("Failed to re-encrypt secure data version id: {}", versionId); + } + } + + try { + Thread.sleep(pauseTimeInMillis); + } catch (InterruptedException e) { + log.error("Failed to sleep between re-encryption", e); + } + } + log.info("Re-encrypted {} secure data and {} secure data version entries.", counter[0], counter[1]); + + } + + @Transactional + protected void reencryptData(String id) { + log.debug("Re-encrypting secure data/file id: {}", id); + Optional secureDataRecordOpt = secureDataDao.readSecureDataByIdLocking(id); + if (! secureDataRecordOpt.isPresent()) { + throw new IllegalArgumentException("No secure data found for id: " + id); + } + + SecureDataRecord secureDataRecord = secureDataRecordOpt.get(); + byte[] ciphertextBytes = secureDataRecord.getEncryptedBlob(); + byte[] reencryptedBytes = reencrypt(secureDataRecord.getType(), ciphertextBytes, secureDataRecord.getPath()); + OffsetDateTime now = dateTimeSupplier.get(); + + secureDataRecord.setLastRotatedTs(now); + secureDataRecord.setEncryptedBlob(reencryptedBytes); + secureDataDao.updateSecureData(secureDataRecord); + } + + @Transactional + protected void reencryptDataVersion(String versionId) { + log.debug("Re-encrypting secure data/file version id: {}", versionId); + + // Lock isn't required when it's the only operation that does update, but just in case. + Optional secureDataVersionRecord = secureDataVersionDao.readSecureDataVersionByIdLocking(versionId); + if (! secureDataVersionRecord.isPresent()) { + throw new IllegalArgumentException("No secure data version found for id: " + versionId); + } + + SecureDataVersionRecord secureDataVersion = secureDataVersionRecord.get(); + String path = secureDataVersion.getPath(); + byte[] ciphertextBytes = secureDataVersion.getEncryptedBlob(); + byte[] reencryptedBytes = reencrypt(secureDataVersion.getType(), ciphertextBytes, path); + OffsetDateTime now = dateTimeSupplier.get(); + + secureDataVersion.setLastRotatedTs(now); + secureDataVersion.setEncryptedBlob(reencryptedBytes); + secureDataVersionDao.updateSecureDataVersion(secureDataVersion); + } + + private byte[] reencrypt(SecureDataType secureDataType, byte[] ciphertextBytes, String path) { + byte[] reencryptedBytes; + if (SecureDataType.OBJECT == secureDataType) { + // Make sure to convert ciphertext to a String first, then decrypt, because Amazon throws an + // error if the ciphertext was encrypted as a String, but is not decrypted as a String. + String ciphertext = new String(ciphertextBytes, StandardCharsets.UTF_8); + String reencryptedCiphertext = encryptionService.reencrypt(ciphertext, path); + reencryptedBytes = reencryptedCiphertext.getBytes(StandardCharsets.UTF_8); + } else if (SecureDataType.FILE == secureDataType) { + reencryptedBytes = encryptionService.reencrypt(ciphertextBytes, path); + } else { + throw new IllegalStateException("Unrecognized data type found at path" + path); + } + return reencryptedBytes; + } } diff --git a/src/main/resources/cms.conf b/src/main/resources/cms.conf index 11038608e..5f6476719 100644 --- a/src/main/resources/cms.conf +++ b/src/main/resources/cms.conf @@ -148,6 +148,12 @@ cms.jobs.configuredJobs=[ "repeatInterval": 1, "repeatTimeUnit": "hours" } + { + "jobClassName": "DataKeyRotationJob", + "repeatCount": -1, # repeat indefinitely + "repeatInterval": 1, + "repeatTimeUnit": "hours" + } ] # Event Processors @@ -169,6 +175,11 @@ cms.jobs.ExpiredTokenCleanUpJob.batchPauseTimeInMillis=0 cms.jobs.KmsCleanUpJob.batchPauseTimeInSeconds=10 cms.jobs.KmsCleanUpJob.deleteKmsKeysOlderThanNDays=30 +# DataKeyRotationJob config +cms.jobs.DataKeyRotationJob.numberOfDataKeyToRotatePerJobRun=120 +cms.jobs.DataKeyRotationJob.dataKeyRotationPauseTimeInMillis=1000 +cms.jobs.DataKeyRotationJob.dataKeyRotationIntervalInDays=90 + # Auth token configuration cms.auth.token.generate.length=64 cms.auth.token.hash.iterations=100 diff --git a/src/main/resources/com/nike/cerberus/mapper/SecureDataMapper.xml b/src/main/resources/com/nike/cerberus/mapper/SecureDataMapper.xml index 31a386460..6332f5d83 100644 --- a/src/main/resources/com/nike/cerberus/mapper/SecureDataMapper.xml +++ b/src/main/resources/com/nike/cerberus/mapper/SecureDataMapper.xml @@ -32,7 +32,8 @@ CREATED_BY, CREATED_TS, LAST_UPDATED_BY, - LAST_UPDATED_TS + LAST_UPDATED_TS, + LAST_ROTATED_TS ) VALUES ( #{record.id}, @@ -45,7 +46,8 @@ #{record.createdBy}, #{record.createdTs}, #{record.lastUpdatedBy}, - #{record.lastUpdatedTs} + #{record.lastUpdatedTs}, + #{record.lastRotatedTs} ) @@ -57,7 +59,8 @@ TOP_LEVEL_KV_COUNT = #{record.topLevelKVCount}, SIZE_IN_BYTES = #{record.sizeInBytes}, LAST_UPDATED_BY = #{record.lastUpdatedBy}, - LAST_UPDATED_TS = #{record.lastUpdatedTs} + LAST_UPDATED_TS = #{record.lastUpdatedTs}, + LAST_ROTATED_TS = #{record.lastRotatedTs} WHERE ID = #{record.id} @@ -73,13 +76,34 @@ CREATED_BY, CREATED_TS, LAST_UPDATED_BY, - LAST_UPDATED_TS + LAST_UPDATED_TS, + LAST_ROTATED_TS From SECURE_DATA WHERE PATH = #{path} AND - SDBOX_ID = #{sdbId} + SDBOX_ID = #{sdbId} for UPDATE + + + + + + SELECT + ID, + SDBOX_ID, + PATH, + ENCRYPTED_BLOB, + `TYPE`, + SIZE_IN_BYTES, + CREATED_BY, + CREATED_TS, + LAST_UPDATED_BY, + LAST_UPDATED_TS + From + SECURE_DATA + WHERE + PATH = #{path} + + + +