From db026229992b0133fd2fdaf3671d59e0d96e3633 Mon Sep 17 00:00:00 2001 From: Tao Liu Date: Tue, 17 Feb 2026 11:07:54 -0500 Subject: [PATCH] Add Java-side cache with large-request bypass for native RNG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a Java-side cache to reduce native RNG calls for small and medium SecureRandom requests. Previously, each nextBytes() call delegated directly to the native RNG, causing frequent calls and extra overhead for workloads that repeatedly request small buffers. With this change: • A large cache is filled from the native RNG and serves small and medium requests, reducing native invocations. • Large requests bypass the cache and are filled directly from the native RNG to avoid extra copy overhead. • After a bypass, the cache is invalidated so later small requests will refill from fresh native call. Signed-off-by: Tao Liu --- .../plus/provider/base/ExtendedRandom.java | 68 ++++++- .../junit/base/BaseTestDRBGCacheBoundary.java | 186 ++++++++++++++++++ .../TestDRBGCacheBoundary_SHA256.java | 25 +++ .../TestDRBGCacheBoundary_SHA256.java | 25 +++ 4 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 src/test/java/ibm/jceplus/junit/base/BaseTestDRBGCacheBoundary.java create mode 100644 src/test/java/ibm/jceplus/junit/openjceplus/TestDRBGCacheBoundary_SHA256.java create mode 100644 src/test/java/ibm/jceplus/junit/openjceplusfips/TestDRBGCacheBoundary_SHA256.java 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"); + } +}