Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<PRNGContextPointer> prngContextBufferSha256 = new ThreadLocal<PRNGContextPointer>();
private static final ThreadLocal<PRNGContextPointer> prngContextBufferSha512 = new ThreadLocal<PRNGContextPointer>();

Expand Down Expand Up @@ -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;
}
}

Expand All @@ -87,6 +132,7 @@ public synchronized void setSeed(byte[] seed) throws NativeException {
if (seed.length > 0) {
createInstanceContextForReSeed();
this.nativeInterface.EXTRAND_setSeed(ockPRNGContextId, seed);
clearRandomByteCache();
}
}

Expand Down Expand Up @@ -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;
}
}
186 changes: 186 additions & 0 deletions src/test/java/ibm/jceplus/junit/base/BaseTestDRBGCacheBoundary.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading