Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,26 @@ public DataSizeNotSupported(String errorMessage) {
}
}

/**
* {@link AesGcmExhaustedException} is thrown during TDF creation when encrypting another
* payload segment would exceed the number of AES-GCM invocations allowed under a single
* data encryption key.
* <p>
* IVs are a random 64-bit fixed field followed by a 32-bit invocation counter (NIST SP
* 800-38D section 8.2.1), so at most 2^32 invocations can use one key without repeating an
* IV: one for the key access metadata and 2^32 - 1 for payload segments. The SDK throws this
* before encrypting the segment that would cross that limit. (The same budget keeps random
* 96-bit IVs, whose collision probability is roughly k^2 / 2^97, under the section 8 limit
* of 2^-32.) The TDF being written is
* incomplete and must be discarded; split the input across multiple TDFs or use a larger
* segment size.
*/
public static class AesGcmExhaustedException extends SDKException {
public AesGcmExhaustedException(String errorMessage) {
super(errorMessage);
}
}

/**
* {@link KasInfoMissing} is thrown during TDF creation when no KAS information is present.
*/
Expand Down
9 changes: 7 additions & 2 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,15 @@ static final class IvCounter {
this.nextInvocation = initialInvocation;
}

/**
* @return the next IV in the sequence, which has never been returned before
* @throws SDK.AesGcmExhaustedException if the per-key invocation budget is spent; this
* is checked before the IV is issued, so no segment is encrypted past the limit
*/
synchronized byte[] next() {
if (exhausted) {
throw new SDKException("exceeded the maximum of 2^32 AES-GCM invocations under a single"
+ " payload key; use a larger segment size");
throw new SDK.AesGcmExhaustedException("exceeded the maximum of 2^32 AES-GCM invocations"
+ " under a single payload key; the TDF is incomplete. Use a larger segment size");
}

long invocation = nextInvocation;
Expand Down
7 changes: 4 additions & 3 deletions sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -963,9 +963,10 @@ public void testIvCounterRejectsReuseAfterExhaustion() {
assertThat(counter.next()).containsExactly(
0, 1, 2, 3, 4, 5, 6, 7,
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff);
SDKException exception = assertThrows(SDKException.class, counter::next);
assertThat(exception).hasMessageContaining("2^32 AES-GCM invocations");
assertThrows(SDKException.class, counter::next);
var exception = assertThrows(SDK.AesGcmExhaustedException.class, counter::next);
assertThat(exception).isInstanceOf(SDKException.class)
.hasMessageContaining("2^32 AES-GCM invocations");
assertThrows(SDK.AesGcmExhaustedException.class, counter::next);
}

// probabilistic: two random 64-bit fixed fields collide with probability 2^-64
Expand Down
Loading