Skip to content

Commit 4b137da

Browse files
committed
Ensure race conditions can't happen on dek zeroing
1 parent bb62bec commit 4b137da

1 file changed

Lines changed: 46 additions & 30 deletions

File tree

src/main/java/com/ironcorelabs/tenantsecurity/kms/v1/CachedKey.java

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import java.util.concurrent.ExecutorService;
1616
import java.util.concurrent.atomic.AtomicBoolean;
1717
import java.util.concurrent.atomic.AtomicInteger;
18-
import java.util.function.Supplier;
18+
import java.util.function.Function;
1919
import java.util.function.ToIntFunction;
2020
import com.ironcorelabs.tenantsecurity.kms.v1.exception.TenantSecurityException;
2121
import 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

Comments
 (0)