1515import java .util .concurrent .ExecutorService ;
1616import java .util .concurrent .atomic .AtomicBoolean ;
1717import java .util .concurrent .atomic .AtomicInteger ;
18- import java .util .function .Supplier ;
18+ import java .util .function .Function ;
1919import java .util .function .ToIntFunction ;
2020import com .ironcorelabs .tenantsecurity .kms .v1 .exception .TenantSecurityException ;
2121import com .ironcorelabs .tenantsecurity .kms .v1 .exception .TscException ;
@@ -72,6 +72,9 @@ public final class CachedKey implements CachedEncryptor, CachedDecryptor {
7272 // Flag to track if close() has been called
7373 private final AtomicBoolean closed = new AtomicBoolean (false );
7474
75+ // Protects the DEK from being read (copied) while close() is zeroing it
76+ private final Object dekLock = new Object ();
77+
7578 // When this cached key was created - used for timeout enforcement
7679 private final Instant createdAt ;
7780
@@ -178,95 +181,107 @@ public int getOperationCount() {
178181 }
179182
180183 /**
181- * Guard an operation with usability checks and operation counting. Verifies the cached key is not
182- * closed or expired before running the operation, and increments the operation count on success.
183- * @param operation The operation to perform
184+ * Guard an operation with usability checks, DEK copy safety, and operation counting. Atomically
185+ * checks that the cached key is not closed and copies the DEK under a lock so that close() cannot
186+ * zero the DEK mid-copy. The copy is zeroed after the operation completes (success or failure).
187+ *
188+ * @param operation The operation to perform, receiving a safe copy of the DEK
184189 * @param countOps Extracts the number of successful operations from the result
185190 * @param counter The counter to increment on success
186191 * @param errorCode The error code to use for closed/expired failures
187192 */
188- private <T > CompletableFuture <T > executeAndIncrement (Supplier <CompletableFuture <T >> operation ,
189- ToIntFunction <T > countOps , AtomicInteger counter , TenantSecurityErrorCodes errorCode ) {
190- if (closed .get ()) {
191- return CompletableFuture
192- .failedFuture (new TscException (errorCode , "CachedKey has been closed" ));
193+ private <T > CompletableFuture <T > executeAndIncrement (
194+ Function <byte [], CompletableFuture <T >> operation , ToIntFunction <T > countOps ,
195+ AtomicInteger counter , TenantSecurityErrorCodes errorCode ) {
196+ byte [] dekCopy ;
197+ synchronized (dekLock ) {
198+ if (closed .get ()) {
199+ return CompletableFuture
200+ .failedFuture (new TscException (errorCode , "CachedKey has been closed" ));
201+ }
202+ dekCopy = Arrays .copyOf (dek , dek .length );
193203 }
194204 if (isExpired ()) {
205+ zeroDek (dekCopy );
195206 return CompletableFuture .failedFuture (new TscException (errorCode , "CachedKey has expired" ));
196207 }
197- return operation .get ().thenApply (result -> {
198- counter .addAndGet (countOps .applyAsInt (result ));
199- return result ;
200- });
208+ return operation .apply (dekCopy ).whenComplete ((result , ex ) -> zeroDek (dekCopy ))
209+ .thenApply (result -> {
210+ counter .addAndGet (countOps .applyAsInt (result ));
211+ return result ;
212+ });
201213 }
202214
203215 @ Override
204216 public CompletableFuture <EncryptedDocument > encrypt (Map <String , byte []> document ,
205217 DocumentMetadata metadata ) {
206218 return executeAndIncrement (
207- () -> DocumentCryptoOps .encryptFields (document , metadata , dek , edek , encryptionExecutor ,
208- secureRandom ),
219+ dekCopy -> DocumentCryptoOps .encryptFields (document , metadata , dekCopy , edek ,
220+ encryptionExecutor , secureRandom ),
209221 result -> 1 , encryptCount , TenantSecurityErrorCodes .DOCUMENT_ENCRYPT_FAILED );
210222 }
211223
212224 @ Override
213225 public CompletableFuture <StreamingResponse > encryptStream (InputStream input , OutputStream output ,
214226 DocumentMetadata metadata ) {
215227 return executeAndIncrement (
216- () -> CompletableFuture .supplyAsync (() -> CryptoUtils
217- .encryptStreamInternal (dek , metadata , input , output , secureRandom ).join (),
218- encryptionExecutor ).thenApply (v -> new StreamingResponse (edek )),
228+ dekCopy -> CompletableFuture
229+ .supplyAsync (
230+ () -> CryptoUtils
231+ .encryptStreamInternal (dekCopy , metadata , input , output , secureRandom ).join (),
232+ encryptionExecutor )
233+ .thenApply (v -> new StreamingResponse (edek )),
219234 result -> 1 , encryptCount , TenantSecurityErrorCodes .DOCUMENT_ENCRYPT_FAILED );
220235 }
221236
222237 @ Override
223238 public CompletableFuture <BatchResult <EncryptedDocument >> encryptBatch (
224239 Map <String , Map <String , byte []>> plaintextDocuments , DocumentMetadata metadata ) {
225- return executeAndIncrement (() -> {
240+ return executeAndIncrement (dekCopy -> {
226241 ConcurrentMap <String , CompletableFuture <EncryptedDocument >> ops = new ConcurrentHashMap <>();
227242 plaintextDocuments .forEach ((id , doc ) -> ops .put (id , DocumentCryptoOps .encryptFields (doc ,
228- metadata , dek , edek , encryptionExecutor , secureRandom )));
243+ metadata , dekCopy , edek , encryptionExecutor , secureRandom )));
229244 return CompletableFuture .supplyAsync (() -> DocumentCryptoOps .cryptoOperationToBatchResult (ops ,
230245 TenantSecurityErrorCodes .DOCUMENT_ENCRYPT_FAILED ));
231246 }, result -> result .getSuccesses ().size (), encryptCount ,
232247 TenantSecurityErrorCodes .DOCUMENT_ENCRYPT_FAILED );
233248 }
234249
235250 private CompletableFuture <PlaintextDocument > validateEdekAndDecrypt (
236- EncryptedDocument encryptedDocument ) {
251+ EncryptedDocument encryptedDocument , byte [] dekCopy ) {
237252 if (!edek .equals (encryptedDocument .getEdek ())) {
238253 return CompletableFuture .failedFuture (new TscException (
239254 TenantSecurityErrorCodes .DOCUMENT_DECRYPT_FAILED , EDEK_MISMATCH_MESSAGE ));
240255 }
241- return DocumentCryptoOps .decryptFields (encryptedDocument .getEncryptedFields (), dek ,
256+ return DocumentCryptoOps .decryptFields (encryptedDocument .getEncryptedFields (), dekCopy ,
242257 encryptedDocument .getEdek (), encryptionExecutor );
243258 }
244259
245260 @ Override
246261 public CompletableFuture <PlaintextDocument > decrypt (EncryptedDocument encryptedDocument ,
247262 DocumentMetadata metadata ) {
248- return executeAndIncrement (() -> validateEdekAndDecrypt (encryptedDocument ), result -> 1 ,
249- decryptCount , TenantSecurityErrorCodes .DOCUMENT_DECRYPT_FAILED );
263+ return executeAndIncrement (dekCopy -> validateEdekAndDecrypt (encryptedDocument , dekCopy ) ,
264+ result -> 1 , decryptCount , TenantSecurityErrorCodes .DOCUMENT_DECRYPT_FAILED );
250265 }
251266
252267 @ Override
253268 public CompletableFuture <Void > decryptStream (String edek , InputStream input , OutputStream output ,
254269 DocumentMetadata metadata ) {
255- return executeAndIncrement (() -> {
270+ return executeAndIncrement (dekCopy -> {
256271 if (!this .edek .equals (edek )) {
257272 return CompletableFuture .<Void >failedFuture (new TscException (
258273 TenantSecurityErrorCodes .DOCUMENT_DECRYPT_FAILED , EDEK_MISMATCH_MESSAGE ));
259274 }
260275 return CompletableFuture .supplyAsync (
261- () -> CryptoUtils .decryptStreamInternal (this . dek , input , output ).join (),
276+ () -> CryptoUtils .decryptStreamInternal (dekCopy , input , output ).join (),
262277 encryptionExecutor );
263278 }, result -> 1 , decryptCount , TenantSecurityErrorCodes .DOCUMENT_DECRYPT_FAILED );
264279 }
265280
266281 @ Override
267282 public CompletableFuture <BatchResult <PlaintextDocument >> decryptBatch (
268283 Map <String , EncryptedDocument > encryptedDocuments , DocumentMetadata metadata ) {
269- return executeAndIncrement (() -> {
284+ return executeAndIncrement (dekCopy -> {
270285 ConcurrentMap <String , CompletableFuture <PlaintextDocument >> ops = new ConcurrentHashMap <>();
271286 ConcurrentMap <String , TenantSecurityException > edekMismatches = new ConcurrentHashMap <>();
272287
@@ -275,7 +290,7 @@ public CompletableFuture<BatchResult<PlaintextDocument>> decryptBatch(
275290 edekMismatches .put (id , new TscException (TenantSecurityErrorCodes .DOCUMENT_DECRYPT_FAILED ,
276291 EDEK_MISMATCH_MESSAGE ));
277292 } else {
278- ops .put (id , DocumentCryptoOps .decryptFields (encDoc .getEncryptedFields (), dek ,
293+ ops .put (id , DocumentCryptoOps .decryptFields (encDoc .getEncryptedFields (), dekCopy ,
279294 encDoc .getEdek (), encryptionExecutor ));
280295 }
281296 });
@@ -302,8 +317,9 @@ public CompletableFuture<BatchResult<PlaintextDocument>> decryptBatch(
302317 @ Override
303318 public void close () {
304319 if (closed .compareAndSet (false , true )) {
305- zeroDek (dek );
306- // Report operations to TSP
320+ synchronized (dekLock ) {
321+ zeroDek (dek );
322+ }
307323 int encrypts = encryptCount .get ();
308324 int decrypts = decryptCount .get ();
309325 if (encrypts > 0 || decrypts > 0 ) {
0 commit comments