diff --git a/src/main/java/com/ibm/crypto/plus/provider/base/ExtendedRandom.java b/src/main/java/com/ibm/crypto/plus/provider/base/ExtendedRandom.java index edb33f291..f9b7be280 100644 --- a/src/main/java/com/ibm/crypto/plus/provider/base/ExtendedRandom.java +++ b/src/main/java/com/ibm/crypto/plus/provider/base/ExtendedRandom.java @@ -21,6 +21,20 @@ public final class ExtendedRandom { private long ockPRNGContextId; private boolean usingThreadLocalContext = true; + // Defaults (in KB) + private static final int DEFAULT_RANDOM_BYTE_CACHE_SIZE_KB = 128; + private static final int DEFAULT_BYPASS_THRESHOLD_KB = 16; + + private static final int RANDOM_BYTE_CACHE_SIZE = Integer.getInteger("openjceplus.randomcachesize", + DEFAULT_RANDOM_BYTE_CACHE_SIZE_KB) * 1024; + + private static final int BYPASS_THRESHOLD = Integer.getInteger("openjceplus.randombypassthreshold", + DEFAULT_BYPASS_THRESHOLD_KB) * 1024; + + private byte[] randomByteCache; + private int cachePos; // Next unread index in cache + private int randomByteCacheLength; + private static final ThreadLocal prngContextBufferSha256 = new ThreadLocal(); private static final ThreadLocal prngContextBufferSha512 = new ThreadLocal(); @@ -74,8 +88,39 @@ public synchronized void nextBytes(byte[] bytes) throws NativeException { throw new IllegalArgumentException("bytes is null"); } - if (bytes.length > 0) { + int len = bytes.length; + if (len == 0) { + return; + } + + // 1) LARGE REQUEST BYPASS: + // Fill destination directly to avoid cache->dest copy cost. + if (len >= BYPASS_THRESHOLD) { this.nativeInterface.EXTRAND_nextBytes(ockPRNGContextId, bytes); + return; + } + + // 2) SMALL/MEDIUM REQUEST: + // Serve from cache, refilling as needed. + int outPos = 0; + int needed = len; + + while (needed > 0) { + int available = randomByteCacheLength - cachePos; + + // If cache is empty (or not initialized), refill it. + if (available <= 0) { + refillRandomByteCache(); + available = randomByteCacheLength - cachePos; + } + + // Copy as much as we can from cache into output. + int toCopy = Math.min(available, needed); + System.arraycopy(randomByteCache, cachePos, bytes, outPos, toCopy); + + cachePos += toCopy; + outPos += toCopy; + needed -= toCopy; } } @@ -87,6 +132,7 @@ public synchronized void setSeed(byte[] seed) throws NativeException { if (seed.length > 0) { createInstanceContextForReSeed(); this.nativeInterface.EXTRAND_setSeed(ockPRNGContextId, seed); + clearRandomByteCache(); } } @@ -129,4 +175,24 @@ long getCtx() { return this.prngCtx; } } + + private void refillRandomByteCache() throws NativeException { + if (randomByteCache == null) { + randomByteCache = new byte[RANDOM_BYTE_CACHE_SIZE]; + } + + // Fill the entire cache from native. + this.nativeInterface.EXTRAND_nextBytes(ockPRNGContextId, randomByteCache); + + cachePos = 0; + randomByteCacheLength = randomByteCache.length; + } + + private void clearRandomByteCache() { + if (randomByteCache != null) { + java.util.Arrays.fill(randomByteCache, (byte) 0); + } + cachePos = 0; + randomByteCacheLength = 0; + } } diff --git a/src/test/java/ibm/jceplus/junit/base/BaseTestDRBGCacheBoundary.java b/src/test/java/ibm/jceplus/junit/base/BaseTestDRBGCacheBoundary.java new file mode 100644 index 000000000..99de41b60 --- /dev/null +++ b/src/test/java/ibm/jceplus/junit/base/BaseTestDRBGCacheBoundary.java @@ -0,0 +1,186 @@ +/* + * Copyright IBM Corp. 2026, 2026 + * + * This code is free software; you can redistribute it and/or modify it + * under the terms provided by IBM in the LICENSE file that accompanied + * this code, including the "Classpath" Exception described therein. + */ + +package ibm.jceplus.junit.base; + +import java.security.SecureRandom; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class BaseTestDRBGCacheBoundary extends BaseTestJunit5 { + + private static final int TEST_CACHE_SIZE_KB = 1; + private static final int TEST_BYPASS_THRESHOLD_SIZE_KB = 2; + + private static final int TEST_BYTE = TEST_CACHE_SIZE_KB * 1024; + private static final int TEST_BYPASS_THRESHOLD_BYTES = TEST_BYPASS_THRESHOLD_SIZE_KB * 1024; + + @BeforeAll + public static void setUp() { + System.setProperty("openjceplus.randomcachesize", + String.valueOf(TEST_CACHE_SIZE_KB)); + System.setProperty("openjceplus.randombypassthreshold", + String.valueOf(TEST_BYPASS_THRESHOLD_SIZE_KB)); + } + + @Test + public void testDRBG_cacheBoundary_sizeMinusOne() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + byte[] bytes = new byte[TEST_BYTE - 1]; + sr.nextBytes(bytes); + + assertEquals(TEST_BYTE - 1, bytes.length, "Unexpected output length"); + assertFalse(allZero(bytes), "Generated bytes should not all be zero"); + } + + @Test + public void testDRBG_cacheBoundary_size() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + byte[] bytes = new byte[TEST_BYTE]; + sr.nextBytes(bytes); + + assertEquals(TEST_BYTE, bytes.length, "Unexpected output length"); + assertFalse(allZero(bytes), "Generated bytes should not all be zero"); + } + + @Test + public void testDRBG_cacheBoundary_sizePlusOne() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + byte[] bytes = new byte[TEST_BYTE + 1]; + sr.nextBytes(bytes); + + assertEquals(TEST_BYTE + 1, bytes.length, "Unexpected output length"); + assertFalse(allZero(bytes), "Generated bytes should not all be zero"); + } + + @Test + public void testDRBG_cacheBoundary_sequenceAroundBoundary() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + // First call leaves 1 byte in cache. + byte[] first = new byte[TEST_BYTE - 1]; + sr.nextBytes(first); + + // Second call onsumes the last cached byte and force a refill + // for the remaining byte. + byte[] second = new byte[2]; + sr.nextBytes(second); + + assertEquals(TEST_BYTE - 1, first.length, "Unexpected first output length"); + assertEquals(2, second.length, "Unexpected second output length"); + + assertFalse(allZero(first), "First generated bytes should not all be zero"); + assertFalse(allZero(second), "Second generated bytes should not all be zero"); + } + + @Test + public void testDRBG_cacheBoundary_outputsDiffer() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + byte[] a = new byte[TEST_BYTE]; + byte[] b = new byte[TEST_BYTE]; + + sr.nextBytes(a); + sr.nextBytes(b); + + assertFalse(Arrays.equals(a, b), "Two sequential outputs should not be identical"); + } + + @Test + public void testDRBG_cacheBoundary_exactDrainThenOneMore() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + // Drain exactly one full cache-sized request. + byte[] first = new byte[TEST_BYTE]; + sr.nextBytes(first); + + // This should require fresh bytes after the exact drain. + byte[] second = new byte[1]; + sr.nextBytes(second); + + assertEquals(TEST_BYTE, first.length, "Unexpected first output length"); + assertEquals(1, second.length, "Unexpected second output length"); + + assertFalse(allZero(first), "First generated bytes should not all be zero"); + assertFalse(allZero(second), "Second generated bytes should not all be zero"); + } + + @Test + public void testDRBG_cacheBoundary_multiStepCrossing() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + // Leave 2 bytes in cache. + byte[] first = new byte[TEST_BYTE - 2]; + sr.nextBytes(first); + + // Leave 1 byte in cache. + byte[] second = new byte[1]; + sr.nextBytes(second); + + // Consume the last cached byte and force a refill for the remaining byte. + byte[] third = new byte[2]; + sr.nextBytes(third); + + assertEquals(TEST_BYTE - 2, first.length, "Unexpected first output length"); + assertEquals(1, second.length, "Unexpected second output length"); + assertEquals(2, third.length, "Unexpected third output length"); + + assertFalse(allZero(first), "First generated bytes should not all be zero"); + assertFalse(allZero(second), "Second generated bytes should not all be zero"); + assertFalse(allZero(third), "Third generated bytes should not all be zero"); + } + + @Test + public void testDRBG_cacheBoundary_repeatedBoundaryRefill() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + for (int i = 0; i < 3; i++) { + byte[] first = new byte[TEST_BYTE - 1]; + sr.nextBytes(first); + + byte[] second = new byte[2]; + sr.nextBytes(second); + + assertEquals(TEST_BYTE - 1, first.length, + "Unexpected first output length in iteration " + i); + assertEquals(2, second.length, + "Unexpected second output length in iteration " + i); + + assertFalse(allZero(first), + "First generated bytes should not all be zero in iteration " + i); + assertFalse(allZero(second), + "Second generated bytes should not all be zero in iteration " + i); + } + } + + @Test + public void testDRBG_bypassSeparation_largeRequest() throws Exception { + SecureRandom sr = SecureRandom.getInstance(getAlgorithm(), getProviderName()); + + byte[] bytes = new byte[TEST_BYPASS_THRESHOLD_BYTES + 1]; + sr.nextBytes(bytes); + + assertEquals(TEST_BYPASS_THRESHOLD_BYTES + 1, bytes.length, "Unexpected output length"); + assertFalse(allZero(bytes), "Generated bytes should not all be zero"); + } + + private static boolean allZero(byte[] input) { + for (byte b : input) { + if (b != 0) { + return false; + } + } + return true; + } +} diff --git a/src/test/java/ibm/jceplus/junit/openjceplus/TestDRBGCacheBoundary_SHA256.java b/src/test/java/ibm/jceplus/junit/openjceplus/TestDRBGCacheBoundary_SHA256.java new file mode 100644 index 000000000..13be82594 --- /dev/null +++ b/src/test/java/ibm/jceplus/junit/openjceplus/TestDRBGCacheBoundary_SHA256.java @@ -0,0 +1,25 @@ +/* + * Copyright IBM Corp. 2026, 2026 + * + * This code is free software; you can redistribute it and/or modify it + * under the terms provided by IBM in the LICENSE file that accompanied + * this code, including the "Classpath" Exception described therein. + */ + +package ibm.jceplus.junit.openjceplus; + +import ibm.jceplus.junit.base.BaseTestDRBGCacheBoundary; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +@TestInstance(Lifecycle.PER_CLASS) +public class TestDRBGCacheBoundary_SHA256 extends BaseTestDRBGCacheBoundary { + + @BeforeAll + public void beforeAll() { + Utils.loadProviderTestSuite(); + setProviderName(Utils.TEST_SUITE_PROVIDER_NAME); + setAlgorithm("SHA256DRBG"); + } +} diff --git a/src/test/java/ibm/jceplus/junit/openjceplusfips/TestDRBGCacheBoundary_SHA256.java b/src/test/java/ibm/jceplus/junit/openjceplusfips/TestDRBGCacheBoundary_SHA256.java new file mode 100644 index 000000000..f204cffe1 --- /dev/null +++ b/src/test/java/ibm/jceplus/junit/openjceplusfips/TestDRBGCacheBoundary_SHA256.java @@ -0,0 +1,25 @@ +/* + * Copyright IBM Corp. 2026, 2026 + * + * This code is free software; you can redistribute it and/or modify it + * under the terms provided by IBM in the LICENSE file that accompanied + * this code, including the "Classpath" Exception described therein. + */ + +package ibm.jceplus.junit.openjceplusfips; + +import ibm.jceplus.junit.base.BaseTestDRBGCacheBoundary; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +@TestInstance(Lifecycle.PER_CLASS) +public class TestDRBGCacheBoundary_SHA256 extends BaseTestDRBGCacheBoundary { + + @BeforeAll + public void beforeAll() { + Utils.loadProviderTestSuite(); + setProviderName(Utils.TEST_SUITE_PROVIDER_NAME); + setAlgorithm("SHA256DRBG"); + } +}