JCE: add optional KEK caching to WolfSSLKeyStore for performance

pull/176/head
Chris Conlon 2025-11-21 11:31:12 -07:00
parent 0a90ee5e91
commit e3fec4658d
7 changed files with 1453 additions and 32 deletions

View File

@ -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

View File

@ -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

View File

@ -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 <n> " +
"Number of getKey() calls (default: 100)");
System.out.println(" -enableCache " +
"Enable KEK caching");
System.out.println(" -cacheTtl <sec> " +
"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);
}
}
}

View File

@ -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 $@

View File

@ -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

View File

@ -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<ByteArrayWrapper, KekCacheEntry> 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<ByteArrayWrapper, KekCacheEntry> 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<ByteArrayWrapper, KekCacheEntry> 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);
}

View File

@ -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");
}
}
}
}