diff --git a/README_JCE.md b/README_JCE.md index a8869402..28acb4fa 100644 --- a/README_JCE.md +++ b/README_JCE.md @@ -36,6 +36,8 @@ file for JCE provider customization: | --- | --- | --- | --- | | wolfjce.wks.iterationCount | 210,000 | Numeric | PBKDF2 iteration count (10,000 minimum) | | wolfjce.wks.maxCertChainLength | 100 | Integer | Max cert chain length | +| wolfjce.keystore.kekCacheEnabled | false | true | Enable KEK caching in WKS KeyStore for performance | +| wolfjce.keystore.kekCacheTtlSec | 300 | Integer | KEK cache TTL in seconds (1 second minimum) | | wolfjce.mapJKStoWKS | UNSET | true | Register fake JKS KeyStore service mapped to WKS | | wolfjce.mapPKCS12toWKS | UNSET | true | Register fake PKCS12 KeyStore service mapped to WKS | @@ -71,6 +73,32 @@ WolfCryptProvider prov = (WolfCryptProvider)Security.getProvider("wolfJCE"); prov.refreshServices(); ``` +**wolfjce.keystore.kekCacheEnabled** - this Security property enables KEK (Key +Encryption Key) caching in the WKS KeyStore to improve performance when making +repeated `getKey()` calls. When disabled (default), each `getKey()` call +performs full PBKDF2 key derivation. When enabled, derived keys are cached in +memory with configurable TTL. The cache is automatically cleared on entry +deletion, overwrite, KeyStore reload, and TTL expiration. For manual cleanup, +call `clearCache()` on the KeyStore instance: + +``` +/* Enable KEK caching with 10 minute TTL */ +Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); +Security.setProperty("wolfjce.keystore.kekCacheTtlSec", "600"); + +KeyStore store = KeyStore.getInstance("WKS", "wolfJCE"); +/* ... use KeyStore ... */ + +/* Explicitly clear cached keys when done (optional) */ +if (store instanceof com.wolfssl.provider.jce.WolfSSLKeyStore) { + ((com.wolfssl.provider.jce.WolfSSLKeyStore) store).clearCache(); +} +``` + +Security Considerations: Cached derived keys remain in memory for the TTL +duration. Only enable in trusted environments where performance benefits +outweigh increased memory exposure. + #### System Property Support The following Java System properties can be set on the command line or diff --git a/docs/design/WolfSSLKeyStore.md b/docs/design/WolfSSLKeyStore.md index 8e2fe280..e90c25d6 100644 --- a/docs/design/WolfSSLKeyStore.md +++ b/docs/design/WolfSSLKeyStore.md @@ -13,6 +13,8 @@ please reference the appropriate Security Policy or contact fips@wolfssl.com. | --- | --- | --- | --- | | `wolfjce.wks.iterationCount` | 210,000 | 10,000 | PBKDF2 iteration count | | `wolfjce.wks.maxCertChainLength` | 100 | N/A | Max cert chain length | +| `wolfjce.keystore.kekCacheEnabled` | false | N/A | Enable KEK caching | +| `wolfjce.keystore.kekCacheTtlSec` | 300 | 1 | Cache TTL in seconds | ## Notes on Algorithm and Security Properties @@ -229,6 +231,88 @@ there is no certificate so no certifiate or private key sanity checks are done. The same encrypt/decrypt process is shared between PrivateKey and SecretKey protection. +## KEK Caching for Performance + +### Overview + +Repeated calls to `getKey()` on the same KeyStore can be slow due to PBKDF2 +happening on each call to derive the Key Encryption Key (KEK) from the user +password. PBKDF2 on each `getKey()` operation ensures that neither password +nor KEK are stored in memory for more time that is needed to derive the KEK and +decrypt the key entry. Although this is the most secure approach, PBKDF2 on +each `getKey()` can be too performance expensive for some use cases. + +The WKS KeyStore includes an optional KEK (Key Encryption Key) cache that +stores derived keys in memory to avoid repeated PBKDF2 computations for the +same password/salt combination. With KEK caching enabled, follow up calls +to `getKey()` are much faster. + +### Cache Design + +The cache uses the following design: + +- **Cache Key:** `SHA-256(passwordHash + kdfSalt + kdfIterations)` + - `passwordHash` = `SHA-256(password)` - avoids storing plaintext passwords + - Including `kdfSalt` and `kdfIterations` ensures different entries with + the same password but different PBKDF2 parameters have separate cache keys +- **Cache Entry:** Stores the derived key (KEK + HMAC key), password hash for + verification, and TTL expiry timestamp +- **Password Verification:** On cache hit, the provided password is hashed and + compared against the stored hash. +- **HMAC Verification:** Caching only occurs after successful HMAC verification + to ensure data integrity is maintained. + +### Security Properties + +| Property | Default | Description | +| --- | --- | --- | +| `wolfjce.keystore.kekCacheEnabled` | `false` | Set to `"true"` to enable caching | +| `wolfjce.keystore.kekCacheTtlSec` | `300` | Cache entry TTL in seconds (5 min) | + +Example usage: + +```java +/* Enable KEK caching with 10 minute TTL */ +Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); +Security.setProperty("wolfjce.keystore.kekCacheTtlSec", "600"); +``` + +### Cache Lifecycle + +The cache is cleared in the following scenarios: +- **Entry deletion:** When `deleteEntry()` is called on an encrypted entry +- **Entry overwrite:** When `setKeyEntry()` overwrites an existing encrypted + entry +- **KeyStore reload:** When `load()` is called to load a new KeyStore +- **TTL expiration:** Individual entries are removed when their TTL expires +- **Explicit clear:** When `clearCache()` is called on the KeyStore instance +- **Garbage collection:** Automatically when the KeyStore object is finalized + +For deterministic cleanup of sensitive cached data, explicitly call +`clearCache()` when the KeyStore is no longer needed: + +```java +KeyStore store = KeyStore.getInstance("WKS", "wolfJCE"); +/* ... use the KeyStore ... */ + +/* Explicitly clear cached keys before discarding */ +if (store instanceof com.wolfssl.provider.jce.WolfSSLKeyStore) { + ((com.wolfssl.provider.jce.WolfSSLKeyStore) store).clearCache(); +} +``` + +### Security Considerations + +1. **Memory exposure:** Cached derived keys remain in memory for the TTL + duration. Only enable in trusted environments where performance benefits + outweigh the increased memory exposure window. + +### Performance Characteristics + +- **First call:** Full PBKDF2 derivation to generate KEK from password +- **Subsequent calls:** Cache lookup and verification +- **Cache overhead:** ~1-2 SHA-256 operations per call for cache key computation + ## Certificate Protection A Certificate entry is stored into the KeyStore with the diff --git a/examples/provider/WolfSSLKeyStoreGetKeyBenchmark.java b/examples/provider/WolfSSLKeyStoreGetKeyBenchmark.java new file mode 100644 index 00000000..02bebb36 --- /dev/null +++ b/examples/provider/WolfSSLKeyStoreGetKeyBenchmark.java @@ -0,0 +1,341 @@ +/* WolfSSLKeyStoreGetKeyBenchmark.java + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.spec.PKCS8EncodedKeySpec; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import com.wolfssl.provider.jce.WolfCryptProvider; + +/** + * Benchmark for WolfSSLKeyStore getKey() performance. + * + * This benchmark measures the time taken to repeatedly call getKey() on + * a single KeyStore object, which exercises the PBKDF2 key derivation path. + * Use this to establish baseline performance before KEK caching, and to + * measure improvements after enabling the cache. + */ +public class WolfSSLKeyStoreGetKeyBenchmark { + + /* Default parameters */ + private static int iterations = 100; + private static boolean enableCache = false; + private static String cacheTtlSec = "300"; + private static boolean testPrivateKey = true; + private static boolean testSecretKey = true; + + /* KeyStore configuration */ + private static String storePass = "benchmarkpassword"; + private static String keyPass = "benchmarkpassword"; + + /* Test files */ + private static String tmpKeyStoreFile = "getkey_benchmark_tmp.wks"; + private static String serverCertDer = "../../certs/server-cert.der"; + private static String serverKeyPkcs8Der = "../../certs/server-keyPkcs8.der"; + + /** + * Print usage information + */ + private static void printUsage() { + System.out.println("WolfSSLKeyStore getKey() Benchmark"); + System.out.println(""); + System.out.println("Usage: java WolfSSLKeyStoreGetKeyBenchmark " + + "[options]"); + System.out.println(""); + System.out.println("Options:"); + System.out.println(" -iterations " + + "Number of getKey() calls (default: 100)"); + System.out.println(" -enableCache " + + "Enable KEK caching"); + System.out.println(" -cacheTtl " + + "Cache TTL in seconds (default: 300)"); + System.out.println(" -privateOnly " + + "Only test private key retrieval"); + System.out.println(" -secretOnly " + + "Only test secret key retrieval"); + System.out.println(" -help " + + "Show this help message"); + System.out.println(""); + System.out.println("Examples:"); + System.out.println(" java WolfSSLKeyStoreGetKeyBenchmark"); + System.out.println(" java WolfSSLKeyStoreGetKeyBenchmark " + + "-iterations 50"); + System.out.println(" java WolfSSLKeyStoreGetKeyBenchmark " + + "-enableCache -iterations 1000"); + } + + /** + * Parse command line arguments + */ + private static void parseArgs(String[] args) { + + for (int i = 0; i < args.length; i++) { + if (args[i].equals("-iterations") && i + 1 < args.length) { + try { + iterations = Integer.parseInt(args[++i]); + } catch (NumberFormatException e) { + System.err.println("Invalid iteration count: " + args[i]); + printUsage(); + System.exit(1); + } + } else if (args[i].equals("-enableCache")) { + enableCache = true; + } else if (args[i].equals("-cacheTtl") && i + 1 < args.length) { + cacheTtlSec = args[++i]; + } else if (args[i].equals("-privateOnly")) { + testPrivateKey = true; + testSecretKey = false; + } else if (args[i].equals("-secretOnly")) { + testPrivateKey = false; + testSecretKey = true; + } else if (args[i].equals("-help") || args[i].equals("--help")) { + printUsage(); + System.exit(0); + } else { + System.err.println("Unknown argument: " + args[i]); + printUsage(); + System.exit(1); + } + } + } + + /** + * Create PrivateKey from DER file + */ + private static PrivateKey loadPrivateKey(String derPath) + throws Exception { + + byte[] keyBytes; + PKCS8EncodedKeySpec spec; + KeyFactory kf; + + keyBytes = Files.readAllBytes(new File(derPath).toPath()); + spec = new PKCS8EncodedKeySpec(keyBytes); + kf = KeyFactory.getInstance("RSA"); + + return kf.generatePrivate(spec); + } + + /** + * Load Certificate from DER file + */ + private static Certificate loadCertificate(String derPath) + throws Exception { + + CertificateFactory cf; + Certificate cert; + + cf = CertificateFactory.getInstance("X.509"); + try (FileInputStream fis = new FileInputStream(derPath)) { + cert = cf.generateCertificate(fis); + } + + return cert; + } + + /** + * Create test KeyStore with entries + */ + private static KeyStore createTestKeyStore() throws Exception { + + KeyStore store; + KeyStore loadedStore; + PrivateKey privKey; + Certificate cert; + KeyGenerator keyGen; + SecretKey secretKey; + + store = KeyStore.getInstance("WKS", "wolfJCE"); + store.load(null, storePass.toCharArray()); + + /* Add private key entry */ + privKey = loadPrivateKey(serverKeyPkcs8Der); + cert = loadCertificate(serverCertDer); + store.setKeyEntry("testPrivateKey", privKey, keyPass.toCharArray(), + new Certificate[] { cert }); + + /* Add secret key entry */ + keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + secretKey = keyGen.generateKey(); + store.setKeyEntry("testSecretKey", secretKey, keyPass.toCharArray(), + null); + + /* Save to file */ + try (FileOutputStream fos = new FileOutputStream(tmpKeyStoreFile)) { + store.store(fos, storePass.toCharArray()); + } + + /* Reload from file to simulate real usage */ + loadedStore = KeyStore.getInstance("WKS", "wolfJCE"); + try (FileInputStream fis = new FileInputStream(tmpKeyStoreFile)) { + loadedStore.load(fis, storePass.toCharArray()); + } + + return loadedStore; + } + + /** + * Run benchmark for a specific key alias + */ + private static void runBenchmark(KeyStore store, String alias, + String keyType) throws Exception { + + long[] times = new long[iterations]; + long totalTime = 0; + long minTime = Long.MAX_VALUE; + long maxTime = 0; + + System.out.println("\nBenchmarking " + keyType + " retrieval:"); + System.out.println(" Alias: " + alias); + System.out.println(" Iterations: " + iterations); + System.out.println(""); + + /* Run benchmark */ + for (int i = 0; i < iterations; i++) { + long startTime = System.nanoTime(); + Key key = store.getKey(alias, keyPass.toCharArray()); + long endTime = System.nanoTime(); + + if (key == null) { + throw new Exception("getKey() returned null for alias: " + + alias); + } + + long elapsed = endTime - startTime; + times[i] = elapsed; + totalTime += elapsed; + + if (elapsed < minTime) minTime = elapsed; + if (elapsed > maxTime) maxTime = elapsed; + + /* Print progress every 10 iterations */ + if ((i + 1) % 10 == 0 || i == 0) { + System.out.printf(" Iteration %d: %.2f ms%n", i + 1, + elapsed / 1_000_000.0); + } + } + + /* Calculate statistics */ + double avgTimeMs = (totalTime / (double) iterations) / 1_000_000.0; + double minTimeMs = minTime / 1_000_000.0; + double maxTimeMs = maxTime / 1_000_000.0; + double totalTimeSec = totalTime / 1_000_000_000.0; + double opsPerSec = iterations / totalTimeSec; + + /* Print results */ + System.out.println(""); + System.out.println("Results for " + keyType + ":"); + System.out.println(" ----------------------------------------"); + System.out.printf(" Total time: %.3f sec%n", totalTimeSec); + System.out.printf(" Average time: %.2f ms/call%n", avgTimeMs); + System.out.printf(" Min time: %.2f ms%n", minTimeMs); + System.out.printf(" Max time: %.2f ms%n", maxTimeMs); + System.out.printf(" Throughput: %.2f ops/sec%n", opsPerSec); + System.out.println(" ----------------------------------------"); + + /* Show first call vs subsequent calls comparison */ + if (iterations > 1) { + double firstCallMs = times[0] / 1_000_000.0; + double avgSubsequent = 0; + for (int i = 1; i < iterations; i++) { + avgSubsequent += times[i]; + } + avgSubsequent = (avgSubsequent / (iterations - 1)) / 1_000_000.0; + + System.out.println(""); + System.out.println(" First call vs subsequent:"); + System.out.printf(" First call: %.2f ms%n", firstCallMs); + System.out.printf(" Avg subsequent: %.2f ms%n", + avgSubsequent); + if (enableCache && avgSubsequent < firstCallMs / 2) { + System.out.printf(" Speedup: %.1fx%n", + firstCallMs / avgSubsequent); + } + } + } + + public static void main(String[] args) { + try { + parseArgs(args); + + System.out.println("==================================="); + System.out.println("WKS getKey() Performance Benchmark"); + System.out.println("==================================="); + System.out.println(""); + + /* Register wolfJCE provider */ + Security.insertProviderAt(new WolfCryptProvider(), 1); + + /* Configure KEK cache if requested */ + if (enableCache) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "true"); + Security.setProperty("wolfjce.keystore.kekCacheTtlSec", + cacheTtlSec); + System.out.println("KEK Cache: ENABLED"); + System.out.println("Cache TTL: " + cacheTtlSec + " seconds"); + } else { + System.out.println("KEK Cache: DISABLED (default)"); + } + System.out.println(""); + + /* Create test KeyStore */ + System.out.println("Creating test KeyStore..."); + KeyStore store = createTestKeyStore(); + System.out.println("KeyStore created with " + store.size() + + " entries"); + + /* Run benchmarks */ + if (testPrivateKey) { + runBenchmark(store, "testPrivateKey", "Private Key"); + } + + if (testSecretKey) { + runBenchmark(store, "testSecretKey", "Secret Key"); + } + + /* Cleanup */ + new File(tmpKeyStoreFile).delete(); + + System.out.println("\n==================================="); + System.out.println("Benchmark complete"); + System.out.println("==================================="); + + } catch (Exception e) { + System.err.println("Benchmark failed: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} + diff --git a/examples/provider/WolfSSLKeyStoreGetKeyBenchmark.sh b/examples/provider/WolfSSLKeyStoreGetKeyBenchmark.sh new file mode 100755 index 00000000..b4d44071 --- /dev/null +++ b/examples/provider/WolfSSLKeyStoreGetKeyBenchmark.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cd ./examples/build/provider +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../../lib:/usr/local/lib +java -classpath ../../../lib/wolfcrypt-jni.jar:./ -Dsun.boot.library.path=../../../lib/ WolfSSLKeyStoreGetKeyBenchmark $@ diff --git a/jni/include/com_wolfssl_provider_jce_WolfSSLKeyStore.h b/jni/include/com_wolfssl_provider_jce_WolfSSLKeyStore.h index 5bea1da5..08092dff 100644 --- a/jni/include/com_wolfssl_provider_jce_WolfSSLKeyStore.h +++ b/jni/include/com_wolfssl_provider_jce_WolfSSLKeyStore.h @@ -31,6 +31,8 @@ extern "C" { #define com_wolfssl_provider_jce_WolfSSLKeyStore_WKS_ENTRY_ID_CERTIFICATE 2L #undef com_wolfssl_provider_jce_WolfSSLKeyStore_WKS_ENTRY_ID_SECRET_KEY #define com_wolfssl_provider_jce_WolfSSLKeyStore_WKS_ENTRY_ID_SECRET_KEY 3L +#undef com_wolfssl_provider_jce_WolfSSLKeyStore_KEK_CACHE_DEFAULT_TTL_MS +#define com_wolfssl_provider_jce_WolfSSLKeyStore_KEK_CACHE_DEFAULT_TTL_MS 300000LL /* * Class: com_wolfssl_provider_jce_WolfSSLKeyStore * Method: X509CheckPrivateKey diff --git a/src/main/java/com/wolfssl/provider/jce/WolfSSLKeyStore.java b/src/main/java/com/wolfssl/provider/jce/WolfSSLKeyStore.java index 35a30a4d..53ac77af 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfSSLKeyStore.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfSSLKeyStore.java @@ -51,7 +51,11 @@ import java.security.cert.X509Certificate; import java.security.cert.CertificateFactory; import java.security.cert.CertificateException; import java.security.cert.CertificateEncodingException; +import java.security.MessageDigest; import java.util.concurrent.ConcurrentHashMap; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.Mac; @@ -161,6 +165,25 @@ import com.wolfssl.wolfcrypt.WolfCryptException; * That HMAC is loaded together with the entry and verified against the * provided password when the entry is retrieved by the user. This is * independent of the entire KeyStore integrity HMAC verification. + * + * KEK Caching for Performance + * + * Repeated calls to {@code getKey()} can be slow due to PBKDF2 key derivation. + * This design is on purpose for security of private keys. An optional KEK + * (Key Encryption Key) cache can be enabled to improve performance by caching + * derived keys in memory. + * + * Security properties controlling KEK caching: + * + * {@code wolfjce.keystore.kekCacheEnabled} - Set to "true" to enable + * caching (default: false/disabled) + * + * {@code wolfjce.keystore.kekCacheTtlSec} - Cache TTL in seconds + * (default: 300 = 5 minutes) + * + * Security Note: Enabling the cache keeps derived keys in memory for the TTL + * duration. Only enable in trusted environments where the performance benefit + * outweighs the increased memory exposure window. */ public class WolfSSLKeyStore extends KeyStoreSpi { @@ -209,6 +232,17 @@ public class WolfSSLKeyStore extends KeyStoreSpi { private static final int WKS_ENTRY_ID_CERTIFICATE = 2; private static final int WKS_ENTRY_ID_SECRET_KEY = 3; + /* Security property name to enable KEK cache (disabled by default) */ + private static final String KEK_CACHE_ENABLED_PROPERTY = + "wolfjce.keystore.kekCacheEnabled"; + + /* Security property name for KEK cache TTL in seconds */ + private static final String KEK_CACHE_TTL_PROPERTY = + "wolfjce.keystore.kekCacheTtlSec"; + + /* Default TTL: 5 minutes in milliseconds */ + private static final long KEK_CACHE_DEFAULT_TTL_MS = 300000; + /** * KeyStore entries as ConcurrentHashMap. * Entry values are objects of one of the following types: @@ -224,6 +258,83 @@ public class WolfSSLKeyStore extends KeyStoreSpi { SECRET_KEY /* WKSSecretKey */ }; + /** + * Cache for derived KEK keys, keyed by SHA-256(passwordHash + kdfSalt + + * kdfIterations). Used to avoid repeated PBKDF2 derivations for the same + * password/salt combination if enabled via Security property. + */ + private final Map kekCache = + new ConcurrentHashMap<>(); + + /* Lock for cache operations */ + private final Object cacheLock = new Object(); + + /** + * KEK cache entry holding derived KEK key and metadata. + */ + private static class KekCacheEntry { + + byte[] derivedKey; /* cached KEK + HMAC key */ + byte[] passHash; /* SHA-256 hash of password */ + long expiryTime; /* System.currentTimeMillis() when entry expires */ + + KekCacheEntry(byte[] derivedKey, byte[] passHash, long expiryTime) { + this.derivedKey = derivedKey.clone(); + this.passHash = passHash.clone(); + this.expiryTime = expiryTime; + } + + synchronized void wipe() { + if (derivedKey != null) { + Arrays.fill(derivedKey, (byte)0); + derivedKey = null; + } + if (passHash != null) { + Arrays.fill(passHash, (byte)0); + passHash = null; + } + expiryTime = 0; + } + } + + /** + * Wrapper for byte arrays to use as kekCache map keys. + */ + private static class ByteArrayWrapper { + + private final byte[] data; + + ByteArrayWrapper(byte[] data) { + this.data = data.clone(); + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + ByteArrayWrapper that = (ByteArrayWrapper)obj; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + return Arrays.hashCode(data); + } + + void wipe() { + if (data != null) { + Arrays.fill(data, (byte)0); + } + } + } + + static { int iCount = WKS_PBKDF2_DEFAULT_ITERATIONS; int cLength = WKS_DEFAULT_MAX_CHAIN_COUNT; @@ -285,6 +396,39 @@ public class WolfSSLKeyStore extends KeyStoreSpi { WKS_STORE_VERSION + ")"); } + /** + * Clear all KEK cache entries. + * + * This method removes all cached derived keys. Should be called when: + * - KeyStore instance is no longer needed + * - Want to ensure cached keys are removed from memory + * - Security policy requires explicit cache clearing + * + * The cache will also be cleared automatically when this KeyStore is + * garbage collected. But calling this method explicitly provides + * deterministic cleanup. + * + * This method is safe to call multiple times and has no effect + * if the cache is already empty. + */ + public void clearCache() { + clearKekCache(); + } + + /** + * Cleanup method to wipe KEK cache when KeyStore is garbage collected. + */ + @SuppressWarnings("deprecation") + @Override + protected void finalize() throws Throwable { + try { + /* Ensure KEK cache is cleared */ + clearCache(); + } finally { + super.finalize(); + } + } + /** * Native JNI method that calls wolfSSL_X509_check_private_key() * to confirm that the provided X.509 certificate matches the given @@ -300,6 +444,354 @@ public class WolfSSLKeyStore extends KeyStoreSpi { private native boolean X509CheckPrivateKey( byte[] derCert, byte[] pkcs8PrivKey) throws WolfCryptException; + /** + * Check if KEK caching is enabled via Security property. + * + * @return true if cache is enabled, false otherwise + */ + private boolean isKekCacheEnabled() { + + String enabled = Security.getProperty(KEK_CACHE_ENABLED_PROPERTY); + + if (enabled != null && enabled.equalsIgnoreCase("true")) { + return true; + } + + return false; + } + + /** + * Get KEK cache TTL from Security property, convert to ms and return. + * + * @return TTL in milliseconds + */ + private long getKekCacheTtlMs() { + + long ttlSec; + String ttlStr = Security.getProperty(KEK_CACHE_TTL_PROPERTY); + + if (ttlStr != null) { + try { + ttlSec = Long.parseLong(ttlStr.trim()); + if (ttlSec > 0) { + /* Convert from sec to ms, checking for overflow */ + if (ttlSec > Long.MAX_VALUE / 1000) { + /* Overflow would occur, return Long.MAX_VALUE */ + return Long.MAX_VALUE; + } + return ttlSec * 1000; + } + } catch (NumberFormatException e) { + log("error parsing " + KEK_CACHE_TTL_PROPERTY + + " property, using default TTL instead"); + } + } + + return KEK_CACHE_DEFAULT_TTL_MS; + } + + /** + * Hash password using SHA-256. + * + * Converts char[] to byte[] without creating an intermediate String. + * + * @param password password to hash + * + * @return SHA-256 hash of password + * + * @throws NoSuchAlgorithmException if SHA-256 not available + */ + private byte[] hashPassword(char[] password) + throws NoSuchAlgorithmException { + + byte[] passBytes = null; + ByteBuffer byteBuffer = null; + CharBuffer charBuffer = null; + MessageDigest md = null; + + try { + /* Convert char[] to byte[] */ + charBuffer = CharBuffer.wrap(password); + byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); + + passBytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(passBytes); + + md = MessageDigest.getInstance("SHA-256"); + return md.digest(passBytes); + + } finally { + if (passBytes != null) { + Arrays.fill(passBytes, (byte)0); + } + if (byteBuffer != null && byteBuffer.hasArray()) { + Arrays.fill(byteBuffer.array(), (byte)0); + } + } + } + + /** + * Generate cache key by hashing: password hash, salt, and iteration count. + * + * Including iteration count ensures entries with different PBKDF2 + * iterations have different cache keys, even if they share the same + * password and salt. + * + * @param passwordHash SHA-256 hash of password + * @param kdfSalt PBKDF2 salt from the entry + * @param kdfIterations PBKDF2 iteration count from the entry + * + * @return SHA-256 hash to use as cache key + * + * @throws NoSuchAlgorithmException if SHA-256 not available + */ + private byte[] generateCacheKey(byte[] passwordHash, byte[] kdfSalt, + int kdfIterations) throws NoSuchAlgorithmException { + + MessageDigest md = MessageDigest.getInstance("SHA-256"); + + md.update(passwordHash); + md.update(kdfSalt); + /* Include iteration count as 4 bytes (big-endian) */ + md.update((byte)(kdfIterations >> 24)); + md.update((byte)(kdfIterations >> 16)); + md.update((byte)(kdfIterations >> 8)); + md.update((byte)(kdfIterations)); + + return md.digest(); + } + + /** + * Retrieve cached derived key for given password, salt, and iterations. + * + * @param password password used for key derivation + * @param kdfSalt PBKDF2 salt from the entry + * @param kdfIterations PBKDF2 iteration count from the entry + * + * @return cached derived key if found and valid, null otherwise + */ + private byte[] getCachedDerivedKey(char[] password, byte[] kdfSalt, + int kdfIterations) { + + long now; + byte[] passHash = null; + byte[] cacheKeyBytes = null; + ByteArrayWrapper lookupKey = null; + KekCacheEntry entryValue = null; + + /* Return null if caching is disabled */ + if (!isKekCacheEnabled()) { + return null; + } + + try { + /* Compute password hash and cache key */ + passHash = hashPassword(password); + cacheKeyBytes = generateCacheKey(passHash, kdfSalt, kdfIterations); + lookupKey = new ByteArrayWrapper(cacheKeyBytes); + + synchronized (cacheLock) { + + entryValue = kekCache.get(lookupKey); + if (entryValue != null) { + /* If cache entry expired, remove and return null */ + now = System.currentTimeMillis(); + if (now >= entryValue.expiryTime) { + + /* Wipe both key and value, then remove from cache */ + kekCache.computeIfPresent(lookupKey, (key, value) -> { + value.wipe(); + key.wipe(); + return null; /* Remove entry */ + }); + + log("Cache entry expired, removed from cache"); + return null; + } + + /* Verify password hash matches */ + if (!MessageDigest.isEqual(passHash, entryValue.passHash)) { + /* Password mismatch - don't use cache */ + return null; + } + + /* Cache hit - return copy of derived key */ + log("Using cached PBKDF2 derived key"); + return entryValue.derivedKey.clone(); + } + } + + return null; + + } catch (NoSuchAlgorithmException e) { + /* If MessageDigest SHA-256 is not available, return null */ + return null; + + } finally { + if (passHash != null) { + Arrays.fill(passHash, (byte)0); + } + if (cacheKeyBytes != null) { + Arrays.fill(cacheKeyBytes, (byte)0); + } + if (lookupKey != null) { + lookupKey.wipe(); + } + } + } + + /** + * Store derived KEK key in cache. + * + * @param password password used for key derivation + * @param kdfSalt PBKDF2 salt from the entry + * @param kdfIterations PBKDF2 iteration count from the entry + * @param derivedKey derived key to cache (KEK + HMAC key) + */ + private void cacheDerivedKey(char[] password, byte[] kdfSalt, + int kdfIterations, byte[] derivedKey) { + + long expiryTime, now, ttl; + byte[] passHash = null; + byte[] cacheKeyBytes = null; + KekCacheEntry entry = null; + KekCacheEntry oldEntry = null; + ByteArrayWrapper mapKey = null; + + /* Return if cache is disabled */ + if (!isKekCacheEnabled()) { + return; + } + + try { + /* Compute password hash and cache key */ + passHash = hashPassword(password); + cacheKeyBytes = generateCacheKey(passHash, kdfSalt, kdfIterations); + + /* Calculate expiry time, checking for overflow */ + now = System.currentTimeMillis(); + ttl = getKekCacheTtlMs(); + if (ttl > Long.MAX_VALUE - now) { + /* Overflow would occur - set to Long.MAX_VALUE */ + expiryTime = Long.MAX_VALUE; + } else { + expiryTime = now + ttl; + } + entry = new KekCacheEntry(derivedKey, passHash, expiryTime); + mapKey = new ByteArrayWrapper(cacheKeyBytes); + + synchronized (cacheLock) { + /* Check for old entry, remove/wipe if found */ + oldEntry = kekCache.get(mapKey); + if (oldEntry != null) { + /* Find and wipe old key, then remove entry */ + for (Map.Entry mapEntry : + kekCache.entrySet()) { + if (mapEntry.getKey().equals(mapKey)) { + ByteArrayWrapper oldKey = mapEntry.getKey(); + kekCache.remove(oldKey); + oldEntry.wipe(); + oldKey.wipe(); + break; + } + } + } + /* Insert new entry */ + kekCache.put(mapKey, entry); + } + + log("Cached PBKDF2 derived key"); + + } catch (NoSuchAlgorithmException e) { + log("Error caching derived key: SHA-256 not available"); + + } finally { + if (passHash != null) { + Arrays.fill(passHash, (byte)0); + } + if (cacheKeyBytes != null) { + Arrays.fill(cacheKeyBytes, (byte)0); + } + } + } + + /** + * Remove specific cache entry on HMAC verification failure. + * + * @param password password used for key derivation + * @param kdfSalt PBKDF2 salt from the entry + * @param kdfIterations PBKDF2 iteration count from the entry + */ + private void invalidateCacheEntry(char[] password, byte[] kdfSalt, + int kdfIterations) { + + byte[] passHash = null; + byte[] cacheKeyBytes = null; + ByteArrayWrapper lookupKey = null; + KekCacheEntry entryValue = null; + + /* Do nothing if caching is disabled */ + if (!isKekCacheEnabled()) { + return; + } + + try { + /* Compute password hash and cache key */ + passHash = hashPassword(password); + cacheKeyBytes = generateCacheKey(passHash, kdfSalt, kdfIterations); + lookupKey = new ByteArrayWrapper(cacheKeyBytes); + + synchronized (cacheLock) { + entryValue = kekCache.get(lookupKey); + + if (entryValue != null) { + /* Wipe both key and value, then remove from cache */ + kekCache.computeIfPresent(lookupKey, (key, value) -> { + value.wipe(); + key.wipe(); + return null; /* Remove entry */ + }); + + log("Invalidated cache entry due to HMAC failure"); + } + } + + } catch (NoSuchAlgorithmException e) { + log("Error invalidating cache entry: SHA-256 not available"); + + } finally { + if (passHash != null) { + Arrays.fill(passHash, (byte)0); + } + if (cacheKeyBytes != null) { + Arrays.fill(cacheKeyBytes, (byte)0); + } + if (lookupKey != null) { + lookupKey.wipe(); + } + } + } + + /** + * Clear all entries from the KEK cache. + */ + private void clearKekCache() { + + synchronized (cacheLock) { + int count = kekCache.size(); + for (Map.Entry entry : + kekCache.entrySet()) { + entry.getValue().wipe(); + entry.getKey().wipe(); + } + kekCache.clear(); + + if (count > 0) { + log("Cleared KEK cache (" + count + " entries wiped)"); + } + } + } + /** * Return entry from internal map that matches alias and type. * @@ -650,7 +1142,8 @@ public class WolfSSLKeyStore extends KeyStoreSpi { try { if (entry instanceof WKSPrivateKey) { - plainKey = ((WKSPrivateKey)entry).getDecryptedKey(password); + plainKey = ((WKSPrivateKey)entry).getDecryptedKey( + password, this); p8Spec = new PKCS8EncodedKeySpec(plainKey); @@ -686,7 +1179,7 @@ public class WolfSSLKeyStore extends KeyStoreSpi { else if (entry instanceof WKSSecretKey) { WKSSecretKey sk = (WKSSecretKey)entry; - plainKey = sk.getDecryptedKey(password); + plainKey = sk.getDecryptedKey(password, this); sKey = new SecretKeySpec(plainKey, sk.keyAlgo); } @@ -1005,6 +1498,7 @@ public class WolfSSLKeyStore extends KeyStoreSpi { byte[] encodedKey = null; WKSPrivateKey privKey = null; WKSSecretKey secretKey = null; + Object existingEntry = null; if (alias == null) { throw new KeyStoreException("Alias cannot be null"); @@ -1020,6 +1514,15 @@ public class WolfSSLKeyStore extends KeyStoreSpi { checkKeyIsSupported(key); + /* Clear old KEK cache entry if we will overwrite one */ + existingEntry = entries.get(alias); + if (existingEntry != null) { + if (existingEntry instanceof WKSPrivateKey || + existingEntry instanceof WKSSecretKey) { + clearKekCache(); + } + } + /* PKCS#8 private key (PrivateKey) or raw key bytes (SecretKey) */ encodedKey = key.getEncoded(); if (encodedKey == null || encodedKey.length == 0) { @@ -1135,8 +1638,18 @@ public class WolfSSLKeyStore extends KeyStoreSpi { public synchronized void engineDeleteEntry(String alias) throws KeyStoreException { + Object entry = null; + log("deleting entry at alias: " + alias); + entry = entries.get(alias); + if (entry != null) { + if (entry instanceof WKSPrivateKey || + entry instanceof WKSSecretKey) { + clearKekCache(); + } + } + entries.remove(alias); } @@ -1648,6 +2161,9 @@ public class WolfSSLKeyStore extends KeyStoreSpi { log("loading KeyStore from InputStream"); + /* Clear any cached KEK entries from previous keystore */ + clearKekCache(); + if (password == null || password.length == 0) { havePass = false; log("KeyStore password not provided, HMAC integrity check " + @@ -2264,9 +2780,10 @@ public class WolfSSLKeyStore extends KeyStoreSpi { * be stored in this object. * * @param password password to use for decryption + * @param keyStore outer KeyStore instance for cache operations */ - protected synchronized byte[] getDecryptedKey(char[] password) - throws UnrecoverableKeyException { + protected synchronized byte[] getDecryptedKey(char[] password, + WolfSSLKeyStore keyStore) throws UnrecoverableKeyException { byte[] plain = null; byte[] derivedKey = null; @@ -2274,6 +2791,7 @@ public class WolfSSLKeyStore extends KeyStoreSpi { byte[] kek = new byte[WKS_ENC_KEY_LENGTH]; byte[] hmacKey = new byte[WKS_HMAC_KEY_LENGTH]; byte[] encoded = null; + boolean fromCache = false; if (password == null || password.length == 0) { throw new UnrecoverableKeyException( @@ -2281,24 +2799,31 @@ public class WolfSSLKeyStore extends KeyStoreSpi { } try { - /* Derive key encryption key from password using - * PBKDF2-HMAC-SHA512. Generate a 96 byte key in total, to - * split between 32-byte AES-CBC-256 key and 64-byte - * HMAC-SHA512 key. */ - derivedKey = deriveKeyFromPassword(password, this.kdfSalt, - this.kdfIterations, - WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH); + /* Try to get derived key from cache */ + derivedKey = keyStore.getCachedDerivedKey(password, + this.kdfSalt, this.kdfIterations); - if (derivedKey == null) { - throw new KeyStoreException( - "Error deriving key decryption key, got null key"); + if (derivedKey != null) { + fromCache = true; + } else { + /* Cache miss, derive encryption key from password using + * PBKDF2-HMAC-SHA512. Generate a 96 byte key in total, + * to split between 32-byte AES-CBC-256 key and 64-byte + * HMAC-SHA512 key. */ + derivedKey = deriveKeyFromPassword(password, this.kdfSalt, + this.kdfIterations, + WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH); + + if (derivedKey == null) { + throw new KeyStoreException( + "Error deriving key decryption key, got null key"); + } } - /* Split key into decrypt + HMAC keys, erase derivedKey */ + /* Split key into decrypt + HMAC keys */ System.arraycopy(derivedKey, 0, kek, 0, kek.length); System.arraycopy(derivedKey, kek.length, hmacKey, 0, hmacKey.length); - Arrays.fill(derivedKey, (byte)0); /* Get encoded byte[] of object class variables without HMAC */ encoded = getEncoded(false); @@ -2315,14 +2840,26 @@ public class WolfSSLKeyStore extends KeyStoreSpi { } /* Verify HMAC first before decrypting key */ - if(!WolfCrypt.ConstantCompare(hmac, this.hmacSha512)) { + if (!WolfCrypt.ConstantCompare(hmac, this.hmacSha512)) { + /* HMAC verification failed */ + if (fromCache) { + /* Invalidate the cache entry that gave us wrong key */ + keyStore.invalidateCacheEntry(password, this.kdfSalt, + this.kdfIterations); + } throw new KeyStoreException( "HMAC verification failed on WKSPrivateKey, entry " + - "corrupted"); + "corrupted or wrong password"); } else { log("HMAC verification successful on WKSPrivateKey"); } + /* HMAC verified, cache the derived key */ + if (!fromCache) { + keyStore.cacheDerivedKey(password, this.kdfSalt, + this.kdfIterations, derivedKey); + } + /* Decrypt encrypted key with KEK */ plain = decryptKey(this.encryptedKey, kek, this.iv); if (plain == null) { @@ -2340,6 +2877,9 @@ public class WolfSSLKeyStore extends KeyStoreSpi { } finally { Arrays.fill(kek, (byte)0); Arrays.fill(hmacKey, (byte)0); + if (derivedKey != null) { + Arrays.fill(derivedKey, (byte)0); + } if (hmac != null) { Arrays.fill(hmac, (byte)0); } @@ -2808,9 +3348,10 @@ public class WolfSSLKeyStore extends KeyStoreSpi { * be stored in this object. * * @param password password to use for decryption + * @param keyStore outer KeyStore instance for cache operations */ - protected synchronized byte[] getDecryptedKey(char[] password) - throws UnrecoverableKeyException { + protected synchronized byte[] getDecryptedKey(char[] password, + WolfSSLKeyStore keyStore) throws UnrecoverableKeyException { byte[] plain = null; byte[] derivedKey = null; @@ -2818,6 +3359,7 @@ public class WolfSSLKeyStore extends KeyStoreSpi { byte[] kek = new byte[WKS_ENC_KEY_LENGTH]; byte[] hmacKey = new byte[WKS_HMAC_KEY_LENGTH]; byte[] encoded = null; + boolean fromCache = false; if (password == null || password.length == 0) { throw new UnrecoverableKeyException( @@ -2825,23 +3367,31 @@ public class WolfSSLKeyStore extends KeyStoreSpi { } try { - /* Derive key encryption key from password using - * PBKDF2-HMAC-SHA512. Generate a 96 byte key in total, split - * between 32-byte AES-CBC-256 and 64-byte HMAC-SHA512 key */ - derivedKey = deriveKeyFromPassword(password, this.kdfSalt, - this.kdfIterations, - WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH); + /* Try to get derived key from cache */ + derivedKey = keyStore.getCachedDerivedKey(password, + this.kdfSalt, this.kdfIterations); - if (derivedKey == null) { - throw new KeyStoreException( - "Error deriving key decryption key, got null key"); + if (derivedKey != null) { + fromCache = true; + } else { + /* Cache miss, derive encryption key from password using + * PBKDF2-HMAC-SHA512. Generate a 96 byte key in total, + * split between 32-byte AES-CBC-256 and 64-byte + * HMAC-SHA512 key. */ + derivedKey = deriveKeyFromPassword(password, this.kdfSalt, + this.kdfIterations, + WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH); + + if (derivedKey == null) { + throw new KeyStoreException( + "Error deriving key decryption key, got null key"); + } } - /* Split key into decrypt + HMAC keys, erase derivedKey */ + /* Split key into decrypt + HMAC keys */ System.arraycopy(derivedKey, 0, kek, 0, kek.length); System.arraycopy(derivedKey, kek.length, hmacKey, 0, hmacKey.length); - Arrays.fill(derivedKey, (byte)0); /* Get encoded byte[] of object class variables without HMAC */ encoded = getEncoded(false); @@ -2859,13 +3409,24 @@ public class WolfSSLKeyStore extends KeyStoreSpi { /* Verify HMAC first before decrypting key */ if (!WolfCrypt.ConstantCompare(hmac, this.hmacSha512)) { + if (fromCache) { + /* Invalidate the cache entry that gave us wrong key */ + keyStore.invalidateCacheEntry(password, this.kdfSalt, + this.kdfIterations); + } throw new KeyStoreException( "HMAC verification failed on WKSSecretKey, entry " + - "corrupted"); + "corrupted or wrong password"); } else { log("HMAC verification successful on WKSSecretKey"); } + /* HMAC verified - now safe to cache the derived key */ + if (!fromCache) { + keyStore.cacheDerivedKey(password, this.kdfSalt, + this.kdfIterations, derivedKey); + } + /* Decrypt encrypted key with KEK */ plain = decryptKey(this.encryptedKey, kek, this.iv); if (plain == null) { @@ -2883,6 +3444,9 @@ public class WolfSSLKeyStore extends KeyStoreSpi { } finally { Arrays.fill(kek, (byte)0); Arrays.fill(hmacKey, (byte)0); + if (derivedKey != null) { + Arrays.fill(derivedKey, (byte)0); + } if (hmac != null) { Arrays.fill(hmac, (byte)0); } diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java index e571a0e0..c55d46f1 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java @@ -2448,5 +2448,402 @@ public class WolfSSLKeyStoreTest { WolfSSLKeyStore wksSpi = new WolfSSLKeyStore(); wksSpi.engineProbe(null); } + + @Test + public void testKekCacheDisabledByDefault() throws Exception { + + /* Ensure cache is disabled by default */ + String enabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + assertTrue("KEK cache should be disabled by default", + (enabled == null) || !enabled.equalsIgnoreCase("true")); + + /* Create and populate KeyStore */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + store.setKeyEntry("rsaKey", serverKeyRsa, storePass.toCharArray(), + new Certificate[] { serverCertRsa }); + + /* Time first getKey() */ + long start = System.currentTimeMillis(); + Key key1 = store.getKey("rsaKey", storePass.toCharArray()); + long first = System.currentTimeMillis() - start; + + /* Time second getKey() - should be similar since cache is disabled */ + start = System.currentTimeMillis(); + Key key2 = store.getKey("rsaKey", storePass.toCharArray()); + long second = System.currentTimeMillis() - start; + + assertNotNull(key1); + assertNotNull(key2); + + /* Without cache, both should be slow (within 30% of each other) */ + assertTrue("Without cache, times should be similar (first=" + + first + "ms, second=" + second + "ms)", + second > first * 0.3); + } + + @Test + public void testKekCacheEnabledImprovePerformance() throws Exception { + + /* Save original properties */ + String origEnabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + String origTtl = Security.getProperty( + "wolfjce.keystore.kekCacheTtlSec"); + + try { + /* Enable cache */ + Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); + Security.setProperty("wolfjce.keystore.kekCacheTtlSec", "300"); + + /* Create and populate KeyStore */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + store.setKeyEntry("rsaKey", serverKeyRsa, storePass.toCharArray(), + new Certificate[] { serverCertRsa }); + + /* First getKey() - populates cache */ + long start = System.currentTimeMillis(); + Key key1 = store.getKey("rsaKey", storePass.toCharArray()); + long first = System.currentTimeMillis() - start; + + /* Second getKey() - should be much faster (cache hit) */ + start = System.currentTimeMillis(); + Key key2 = store.getKey("rsaKey", storePass.toCharArray()); + long second = System.currentTimeMillis() - start; + + assertNotNull(key1); + assertNotNull(key2); + assertEquals(key1, key2); + + /* Cache hit should be significantly faster */ + assertTrue("Cache hit should be faster: " + + "first=" + first + "ms, second=" + second + "ms", + first > 0 && (first > 50 ? second < first / 5 : true)); + + } finally { + /* Restore original properties */ + if (origEnabled != null) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + origEnabled); + } else { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "false"); + } + if (origTtl != null) { + Security.setProperty("wolfjce.keystore.kekCacheTtlSec", + origTtl); + } + } + } + + @Test + public void testKekCacheWorksForSecretKey() throws Exception { + + String origEnabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + + try { + /* Enable cache */ + Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); + + /* Create and populate KeyStore with SecretKey */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey secretKey = keyGen.generateKey(); + store.setKeyEntry("aesKey", secretKey, storePass.toCharArray(), + null); + + /* First getKey() */ + long start = System.currentTimeMillis(); + Key key1 = store.getKey("aesKey", storePass.toCharArray()); + long first = System.currentTimeMillis() - start; + + /* Second getKey() - cache hit */ + start = System.currentTimeMillis(); + Key key2 = store.getKey("aesKey", storePass.toCharArray()); + long second = System.currentTimeMillis() - start; + + assertNotNull(key1); + assertNotNull(key2); + + /* Cache hit should be faster */ + assertTrue("SecretKey cache hit should be faster: " + + "first=" + first + "ms, second=" + second + "ms", + first > 50 ? second < first / 5 : true); + + } finally { + if (origEnabled != null) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + origEnabled); + } else { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "false"); + } + } + } + + @Test + public void testKekCacheInvalidateOnDeleteEntry() throws Exception { + + String origEnabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + + try { + /* Enable cache */ + Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); + + /* Create KeyStore with two entries */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + store.setKeyEntry("rsaKey1", serverKeyRsa, storePass.toCharArray(), + new Certificate[] { serverCertRsa }); + store.setKeyEntry("eccKey1", serverKeyEcc, storePass.toCharArray(), + eccServerChain); + + /* Populate cache for both entries */ + Key key1 = store.getKey("rsaKey1", storePass.toCharArray()); + assertNotNull(key1); + Key key2 = store.getKey("eccKey1", storePass.toCharArray()); + assertNotNull(key2); + + /* Verify second call is fast (cache hit) */ + long start = System.currentTimeMillis(); + store.getKey("eccKey1", storePass.toCharArray()); + long cachedTime = System.currentTimeMillis() - start; + + /* Delete first entry - should clear entire cache */ + store.deleteEntry("rsaKey1"); + + /* Verify first entry is deleted */ + assertFalse(store.containsAlias("rsaKey1")); + + /* Get second key again - should be slow (cache was cleared) */ + start = System.currentTimeMillis(); + Key key2Again = store.getKey("eccKey1", storePass.toCharArray()); + long uncachedTime = System.currentTimeMillis() - start; + assertNotNull(key2Again); + + /* Verify it was slower */ + assertTrue("Cache should have been cleared, but timing suggests " + + "it wasn't (cached: " + cachedTime + "ms, uncached: " + + uncachedTime + "ms)", + uncachedTime > cachedTime * 5); + + } finally { + if (origEnabled != null) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + origEnabled); + } else { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "false"); + } + } + } + + @Test + public void testKekCacheInvalidateOnSetKeyEntryOverwrite() + throws Exception { + + String origEnabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + + try { + /* Enable cache */ + Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); + + /* Create KeyStore with two entries */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + store.setKeyEntry("rsaKey1", serverKeyRsa, storePass.toCharArray(), + new Certificate[] { serverCertRsa }); + store.setKeyEntry("eccKey1", serverKeyEcc, storePass.toCharArray(), + eccServerChain); + + /* Populate cache for both entries */ + Key key1 = store.getKey("rsaKey1", storePass.toCharArray()); + assertNotNull(key1); + Key key2 = store.getKey("eccKey1", storePass.toCharArray()); + assertNotNull(key2); + + /* Verify second call is fast (cache hit) */ + long start = System.currentTimeMillis(); + store.getKey("eccKey1", storePass.toCharArray()); + long cachedTime = System.currentTimeMillis() - start; + + /* Overwrite first entry - should clear entire cache */ + store.setKeyEntry("rsaKey1", serverKeyEcc, storePass.toCharArray(), + eccServerChain); + + /* Get second key again - should be slow (cache was cleared) */ + start = System.currentTimeMillis(); + Key key2Again = store.getKey("eccKey1", storePass.toCharArray()); + long uncachedTime = System.currentTimeMillis() - start; + assertNotNull(key2Again); + + /* Verify it was slower */ + assertTrue("Cache should have been cleared, but timing suggests " + + "it wasn't (cached: " + cachedTime + "ms, uncached: " + + uncachedTime + "ms)", + uncachedTime > cachedTime * 5); + + } finally { + if (origEnabled != null) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + origEnabled); + } else { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "false"); + } + } + } + + @Test + public void testKekCacheInvalidateOnLoad() throws Exception { + + String origEnabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + + try { + /* Enable cache */ + Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); + + /* Create first KeyStore and populate cache */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + store.setKeyEntry("rsaKey", serverKeyRsa, storePass.toCharArray(), + new Certificate[] { serverCertRsa }); + + /* Populate cache */ + Key key1 = store.getKey("rsaKey", storePass.toCharArray()); + assertNotNull(key1); + + /* Save to byte array */ + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + store.store(baos, storePass.toCharArray()); + byte[] storeData = baos.toByteArray(); + + /* Load from byte array - should clear cache */ + ByteArrayInputStream bais = new ByteArrayInputStream(storeData); + store.load(bais, storePass.toCharArray()); + + /* Get key - first call after load should take PBKDF2 time */ + long start = System.currentTimeMillis(); + Key key2 = store.getKey("rsaKey", storePass.toCharArray()); + long elapsed = System.currentTimeMillis() - start; + + assertNotNull(key2); + /* After load, should need full PBKDF2 (cache was cleared) */ + /* Use >= 5ms threshold for CI timing variability */ + assertTrue("After load, getKey should take PBKDF2 time: " + + elapsed + "ms", elapsed >= 5); + + } finally { + if (origEnabled != null) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + origEnabled); + } else { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "false"); + } + } + } + + @Test + public void testKekCacheWrongPasswordReturnsMiss() throws Exception { + + String origEnabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + + try { + /* Enable cache */ + Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); + + /* Create and populate KeyStore */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + store.setKeyEntry("rsaKey", serverKeyRsa, storePass.toCharArray(), + new Certificate[] { serverCertRsa }); + + /* Populate cache with correct password */ + Key key1 = store.getKey("rsaKey", storePass.toCharArray()); + assertNotNull(key1); + + /* Try wrong password - should fail, UnrecoverableKeyException */ + try { + store.getKey("rsaKey", "wrongpassword".toCharArray()); + fail("Expected UnrecoverableKeyException for wrong password"); + } catch (UnrecoverableKeyException e) { + /* Expected */ + } + + /* Correct password should still work */ + Key key2 = store.getKey("rsaKey", storePass.toCharArray()); + assertNotNull(key2); + assertEquals(key1, key2); + + } finally { + if (origEnabled != null) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + origEnabled); + } else { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "false"); + } + } + } + + @Test + public void testKekCacheMultipleEntriesSamePassword() throws Exception { + + String origEnabled = Security.getProperty( + "wolfjce.keystore.kekCacheEnabled"); + + try { + /* Enable cache */ + Security.setProperty("wolfjce.keystore.kekCacheEnabled", "true"); + + /* Create KeyStore with multiple entries using same password */ + KeyStore store = KeyStore.getInstance(storeType, storeProvider); + store.load(null, storePass.toCharArray()); + + store.setKeyEntry("rsaKey", serverKeyRsa, storePass.toCharArray(), + new Certificate[] { serverCertRsa }); + store.setKeyEntry("eccKey", serverKeyEcc, storePass.toCharArray(), + eccServerChain); + + /* Populate cache for both entries */ + Key rsaKey = store.getKey("rsaKey", storePass.toCharArray()); + Key eccKey = store.getKey("eccKey", storePass.toCharArray()); + + assertNotNull(rsaKey); + assertNotNull(eccKey); + + /* Both keys should be retrievable again quickly */ + long start = System.currentTimeMillis(); + Key rsaKey2 = store.getKey("rsaKey", storePass.toCharArray()); + Key eccKey2 = store.getKey("eccKey", storePass.toCharArray()); + long elapsed = System.currentTimeMillis() - start; + + assertNotNull(rsaKey2); + assertNotNull(eccKey2); + + assertTrue("Multiple cache hits should be fast: " + + elapsed + "ms", elapsed < 100); + + } finally { + if (origEnabled != null) { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + origEnabled); + } else { + Security.setProperty("wolfjce.keystore.kekCacheEnabled", + "false"); + } + } + } }