diff --git a/JenkinsfilePerformance b/JenkinsfilePerformance index 98b08644c..b90f20830 100644 --- a/JenkinsfilePerformance +++ b/JenkinsfilePerformance @@ -385,6 +385,7 @@ pipeline { 'ibm.jceplus.jmh.PBEBenchmark', \ 'ibm.jceplus.jmh.PBKDF2Benchmark', \ 'ibm.jceplus.jmh.RandomBenchmark', \ + 'ibm.jceplus.jmh.RandomNewInstanceBenchmark', \ 'ibm.jceplus.jmh.RSACipherBenchmark', \ 'ibm.jceplus.jmh.RSAKeyGeneratorBenchmark', \ 'ibm.jceplus.jmh.RSASignatureBenchmark', \ 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 a2eaf2d2f..9a8e59e2d 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 @@ -14,9 +14,21 @@ public final class ExtendedRandom { + private static final boolean IS_ZOS = System.getProperty("os.name") + .toLowerCase() + .contains("z/os"); + private OpenJCEPlusProvider provider; private NativeInterface nativeInterface; - final long ockPRNGContextId; + private final String algName; + + private long ockPRNGContextId; + private boolean usingThreadLocalContext = true; + + private static final ThreadLocal prngContextBufferSha256 = + IS_ZOS ? null : new ThreadLocal(); + private static final ThreadLocal prngContextBufferSha512 = + IS_ZOS ? null : new ThreadLocal(); public static ExtendedRandom getInstance(String algName, OpenJCEPlusProvider provider) throws NativeException { @@ -32,11 +44,42 @@ public static ExtendedRandom getInstance(String algName, OpenJCEPlusProvider pro } private ExtendedRandom(String algName, OpenJCEPlusProvider provider) throws NativeException { + this.algName = algName; this.provider = provider; this.nativeInterface = provider.isFIPS() ? NativeOCKAdapterFIPS.getInstance() : NativeOCKAdapterNonFIPS.getInstance(); - this.ockPRNGContextId = this.nativeInterface.EXTRAND_create(algName); + this.ockPRNGContextId = getPRNGContext(algName); + } + + private long getPRNGContext(String algName) throws NativeException { + // On z/OS, create a new instance context without caching. + if (IS_ZOS) { + return createInstanceContext(); + } + + // On non-z/OS platforms, use the cached context if available, + // otherwise create and cache it. + PRNGContextPointer prngCtx = null; + ThreadLocal prngCtxBuffer = null; + + switch (algName) { + case "SHA256": + prngCtxBuffer = prngContextBufferSha256; + break; + case "SHA512": + prngCtxBuffer = prngContextBufferSha512; + break; + default: + throw new IllegalArgumentException( + "Unsupported HASHDRBG algorithm: " + algName); + } + + prngCtx = prngCtxBuffer.get(); + if (prngCtx == null) { + prngCtx = new PRNGContextPointer(algName, this.nativeInterface, this.provider); + prngCtxBuffer.set(prngCtx); + } - this.provider.registerCleanable(this, cleanOCKResources(ockPRNGContextId, nativeInterface)); + return prngCtx.getCtx(); } public synchronized void nextBytes(byte[] bytes) throws NativeException { @@ -55,11 +98,22 @@ public synchronized void setSeed(byte[] seed) throws NativeException { } if (seed.length > 0) { + // Switch from cached context to instance context for re-seeding + if (usingThreadLocalContext) { + this.ockPRNGContextId = createInstanceContext(); + } this.nativeInterface.EXTRAND_setSeed(ockPRNGContextId, seed); } } - private Runnable cleanOCKResources(long ockPRNGContextId, NativeInterface nativeInterface) { + private long createInstanceContext() throws NativeException { + long instanceCtx = this.nativeInterface.EXTRAND_create(algName); + this.usingThreadLocalContext = false; + this.provider.registerCleanable(this, cleanOCKResources(instanceCtx, nativeInterface)); + return instanceCtx; + } + + private static Runnable cleanOCKResources(long ockPRNGContextId, NativeInterface nativeInterface) { return () -> { try { if (ockPRNGContextId != 0) { @@ -67,10 +121,23 @@ private Runnable cleanOCKResources(long ockPRNGContextId, NativeInterface native } } catch (Exception e) { if (OpenJCEPlusProvider.getDebug() != null) { - OpenJCEPlusProvider.getDebug().println("An error occurred while cleaning : " + e.getMessage()); + OpenJCEPlusProvider.getDebug().println("An error occurred while cleaning: " + e.getMessage()); e.printStackTrace(); } } }; } + + private static final class PRNGContextPointer { + final long prngCtx; + + PRNGContextPointer(String algName, NativeInterface nativeInterface, OpenJCEPlusProvider provider) throws NativeException { + this.prngCtx = nativeInterface.EXTRAND_create(algName); + provider.registerCleanable(this, ExtendedRandom.cleanOCKResources(this.prngCtx, nativeInterface)); + } + + long getCtx() { + return this.prngCtx; + } + } } diff --git a/src/test/java/ibm/jceplus/jmh/RandomNewInstanceBenchmark.java b/src/test/java/ibm/jceplus/jmh/RandomNewInstanceBenchmark.java new file mode 100644 index 000000000..22970e380 --- /dev/null +++ b/src/test/java/ibm/jceplus/jmh/RandomNewInstanceBenchmark.java @@ -0,0 +1,67 @@ +/* + * 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.jmh; + +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 30, timeUnit = TimeUnit.SECONDS) +public class RandomNewInstanceBenchmark extends JMHBase { + + @Param({"16", "2048", "32768"}) + private int payloadSize; + + private byte[] payload; + + private String algorithm; + private String provider; + + @Param({"SHA256DRBG|OpenJCEPlus", "SHA512DRBG|OpenJCEPlus", "SHA1PRNG|SUN", "DRBG|SUN"}) + private String randomToTest; + + @Setup + public void setup() throws Exception { + String[] algAndProvider = randomToTest.split("\\|"); + algorithm = algAndProvider[0]; + provider = algAndProvider[1]; + super.setup(provider); + payload = new byte[payloadSize]; + } + + @Benchmark + public byte[] runSecureRandom() throws Exception { + SecureRandom random = SecureRandom.getInstance(algorithm, provider); + random.nextBytes(payload); + return payload; + } + + public static void main(String[] args) throws RunnerException { + String testSimpleName = RandomNewInstanceBenchmark.class.getSimpleName(); + Options opt = optionsBuild(testSimpleName, testSimpleName); + + new Runner(opt).run(); + } +}