JCE: add optional KEK caching to WolfSSLKeyStore for performance
parent
0a90ee5e91
commit
e3fec4658d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 $@
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue