Skip to content

Commit 501b523

Browse files
authored
MOSIP-44825 (#537)
* Fixed the keymanager memory spike GC pressure issue Signed-off-by: Dhanendra Sahu <dhanendra.tech@gmail.com> * Fixed the keymanager memory spike GC pressure issue Signed-off-by: Dhanendra Sahu <dhanendra.tech@gmail.com> --------- Signed-off-by: Dhanendra Sahu <dhanendra.tech@gmail.com>
1 parent a18a58b commit 501b523

6 files changed

Lines changed: 86 additions & 41 deletions

File tree

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/cryptomanager/util/CryptomanagerUtils.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ public class CryptomanagerUtils {
6464

6565
private static ObjectMapper mapper = JsonMapper.builder().addModule(new AfterburnerModule()).build();
6666

67+
// Single shared instance seeded once at JVM startup. SecureRandom.nextBytes()
68+
// is thread-safe (synchronized internally). Static so it survives @RefreshScope
69+
// bean recreation and avoids re-seeding overhead (and potential entropy
70+
// starvation) at 150 new instances/sec under load.
71+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
72+
6773
/** The Constant UTC_DATETIME_PATTERN. */
6874
private static final String UTC_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
6975

@@ -235,8 +241,7 @@ public byte[] concatCertThumbprint(byte[] certThumbprint, byte[] encryptedKey){
235241

236242
public byte[] generateRandomBytes(int size) {
237243
byte[] randomBytes = new byte[size];
238-
SecureRandom secureRandom = new SecureRandom();
239-
secureRandom.nextBytes(randomBytes);
244+
SECURE_RANDOM.nextBytes(randomBytes);
240245
return randomBytes;
241246
}
242247

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/keymanagerservice/config/ReqResFilter.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import jakarta.servlet.http.HttpServletResponse;
1313

1414
import org.springframework.web.util.ContentCachingRequestWrapper;
15-
import org.springframework.web.util.ContentCachingResponseWrapper;
1615

1716
/**
1817
* This class is for input logging of all parameters in HTTP requests
@@ -32,18 +31,17 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
3231
throws IOException, ServletException {
3332
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
3433
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
35-
ContentCachingRequestWrapper requestWrapper = null;
36-
ContentCachingResponseWrapper responseWrapper = null;
37-
3834
// Default processing for url ends with .stream
3935
if (httpServletRequest.getRequestURI().endsWith(".stream")) {
4036
chain.doFilter(request, response);
4137
return;
4238
}
43-
requestWrapper = new ContentCachingRequestWrapper(httpServletRequest);
44-
responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
45-
chain.doFilter(requestWrapper, responseWrapper);
46-
responseWrapper.copyBodyToResponse();
39+
// Cache only the first 4096 bytes — sufficient for JSON metadata (id, version)
40+
// without buffering the full encrypted payload in memory at high RPS.
41+
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpServletRequest, 4096);
42+
// Pass the actual response directly; response body buffering is not required
43+
// since ResponseBodyAdviceConfig only reads request metadata, not the response.
44+
chain.doFilter(requestWrapper, httpServletResponse);
4745

4846
}
4947

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/keymanagerservice/config/ResponseBodyAdviceConfig.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.mosip.kernel.keymanagerservice.config;
22

3+
import jakarta.annotation.PostConstruct;
34
import jakarta.servlet.http.HttpServletRequest;
45
import jakarta.servlet.http.HttpServletRequestWrapper;
56

@@ -36,9 +37,17 @@ public class ResponseBodyAdviceConfig implements ResponseBodyAdvice<ResponseWrap
3637
@Autowired
3738
private ObjectMapper objectMapper;
3839

40+
@PostConstruct
41+
public void init() {
42+
// Register JavaTimeModule once at startup on the shared singleton ObjectMapper.
43+
// Registering per-request (150 RPS) creates object churn and mutates shared
44+
// state concurrently — both corrected here.
45+
objectMapper.registerModule(new JavaTimeModule());
46+
}
47+
3948
/*
4049
* (non-Javadoc)
41-
*
50+
*
4251
* @see
4352
* org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#
4453
* supports(org.springframework.core.MethodParameter, java.lang.Class)
@@ -79,7 +88,6 @@ public ResponseWrapper<?> beforeBodyWrite(ResponseWrapper<?> body, MethodParamet
7988
.getContentAsByteArray());
8089
}
8190

82-
objectMapper.registerModule(new JavaTimeModule());
8391
if (!EmptyCheckUtils.isNullEmpty(requestBody)) {
8492
requestWrapper = objectMapper.readValue(requestBody, RequestWrapper.class);
8593
body.setId(requestWrapper.getId());

kernel/kernel-keymanager-service/src/main/java/io/mosip/kernel/keymanagerservice/helper/PrivateKeyDecryptorHelper.java

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
import java.security.cert.Certificate;
99
import java.security.spec.InvalidKeySpecException;
1010
import java.security.spec.PKCS8EncodedKeySpec;
11-
import java.util.Map;
1211
import java.util.Objects;
13-
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.TimeUnit;
1413

14+
import jakarta.annotation.PostConstruct;
15+
16+
import org.cache2k.Cache;
17+
import org.cache2k.Cache2kBuilder;
1518
import org.springframework.beans.factory.annotation.Autowired;
19+
import org.springframework.beans.factory.annotation.Value;
1620
import org.springframework.stereotype.Component;
1721

1822
import io.mosip.kernel.core.crypto.exception.InvalidDataException;
@@ -33,7 +37,7 @@
3337

3438
/**
3539
* Private key decryption Helper class for Keymanager
36-
*
40+
*
3741
* @author Mahammed Taheer
3842
* @since 1.2.1
3943
*
@@ -43,9 +47,29 @@ public class PrivateKeyDecryptorHelper {
4347

4448
private static final Logger LOGGER = KeymanagerLogger.getLogger(PrivateKeyDecryptorHelper.class);
4549

46-
private Map<String, io.mosip.kernel.keymanagerservice.entity.KeyStore> cacheKeyStore = new ConcurrentHashMap<>();
50+
/**
51+
* Holds the DB KeyStore entry and its resolved referenceId together so that
52+
* both are always evicted atomically. Using two separate maps would allow
53+
* LRU eviction to evict one without the other, causing spurious
54+
* APP_ID_REFERENCE_ID_NOT_MATCHING exceptions.
55+
*/
56+
private static final class CacheEntry {
57+
final KeyStore keyStore;
58+
final String refId;
59+
CacheEntry(KeyStore keyStore, String refId) {
60+
this.keyStore = keyStore;
61+
this.refId = refId;
62+
}
63+
}
64+
65+
// Replaces the previous unbounded ConcurrentHashMaps (cacheKeyStore +
66+
// cacheReferenceIds). Bounded by entryCapacity so memory does not grow
67+
// indefinitely as new partner certificates are registered over time.
68+
private Cache<String, CacheEntry> cacheDecryptData = null;
4769

48-
private Map<String, String> cacheReferenceIds = new ConcurrentHashMap<>();
70+
// Reuses the same property as keyAliasCache for consistent cache lifecycle.
71+
@Value("${mosip.kernel.keymanager.key.cache.expire.inMins:1440}")
72+
private long cacheExpireInMins;
4973

5074
/**
5175
* Utility to generate Metadata
@@ -59,35 +83,45 @@ public class PrivateKeyDecryptorHelper {
5983
@Autowired
6084
private ECKeyStore keyStore;
6185

86+
@PostConstruct
87+
public void init() {
88+
cacheDecryptData = new Cache2kBuilder<String, CacheEntry>() {}
89+
// hashCode suffix prevents name collision in test contexts where the Spring
90+
// context is reloaded multiple times within the same JVM.
91+
.name("privateKeyDecryptorData-" + this.hashCode())
92+
.expireAfterWrite(cacheExpireInMins, TimeUnit.MINUTES)
93+
// 1000 entries covers large MOSIP deployments with many partner certificates
94+
// while bounding the heap footprint (~3-5 KB per entry × 1000 = ~3-5 MB max).
95+
.entryCapacity(1000)
96+
.build();
97+
}
98+
6299
public KeyStore getDBKeyStoreData (String certThumbprintHex, String applicationId, String referenceId) {
63100

64-
KeyStore dbKeyStore = cacheKeyStore.getOrDefault(certThumbprintHex, null);
65-
66-
String appIdRefIdKey = applicationId + KeymanagerConstant.HYPHEN + referenceId;
67-
String compMasterKeyRefId = applicationId + KeymanagerConstant.HYPHEN + KeymanagerConstant.COMPONENT_MASTER_KEY_DUMMY_REF;
68-
if(Objects.isNull(dbKeyStore)) {
69-
dbKeyStore = dbHelper.getKeyAlias(certThumbprintHex, appIdRefIdKey, applicationId, referenceId);
70-
cacheKeyStore.put(certThumbprintHex, dbKeyStore);
71-
// Added condition to handle issue related to decryption error with Master key.
72-
if (Objects.isNull(dbKeyStore.getPrivateKey())) {
73-
cacheReferenceIds.put(certThumbprintHex, compMasterKeyRefId);
74-
} else {
75-
cacheReferenceIds.put(certThumbprintHex, appIdRefIdKey);
76-
}
77-
}
101+
String appIdRefIdKey = applicationId + KeymanagerConstant.HYPHEN + referenceId;
102+
String compMasterKeyRefId = applicationId + KeymanagerConstant.HYPHEN + KeymanagerConstant.COMPONENT_MASTER_KEY_DUMMY_REF;
103+
104+
CacheEntry cacheEntry = cacheDecryptData.get(certThumbprintHex);
105+
if (Objects.isNull(cacheEntry)) {
106+
KeyStore dbKeyStore = dbHelper.getKeyAlias(certThumbprintHex, appIdRefIdKey, applicationId, referenceId);
107+
// Added condition to handle issue related to decryption error with Master key.
108+
String refIdToCache = Objects.isNull(dbKeyStore.getPrivateKey()) ? compMasterKeyRefId : appIdRefIdKey;
109+
cacheEntry = new CacheEntry(dbKeyStore, refIdToCache);
110+
cacheDecryptData.put(certThumbprintHex, cacheEntry);
111+
}
78112

79-
String cachedRefId = cacheReferenceIds.getOrDefault(certThumbprintHex, null);
80-
if (!appIdRefIdKey.equals(cachedRefId) && !compMasterKeyRefId.equals(cachedRefId)){
113+
String cachedRefId = cacheEntry.refId;
114+
if (!appIdRefIdKey.equals(cachedRefId) && !compMasterKeyRefId.equals(cachedRefId)){
81115
LOGGER.error(KeymanagerConstant.SESSIONID, this.getClass().getSimpleName(), KeymanagerConstant.EMPTY,
82116
"Application Id & Reference ID not matching with the input thumbprint value(decrypt).");
83117
throw new KeymanagerServiceException(KeymanagerErrorConstant.APP_ID_REFERENCE_ID_NOT_MATCHING.getErrorCode(),
84118
KeymanagerErrorConstant.APP_ID_REFERENCE_ID_NOT_MATCHING.getErrorMessage());
85119
}
86-
return dbKeyStore;
120+
return cacheEntry.keyStore;
87121
}
88122

89123
public Object[] getKeyObjects(KeyStore dbKeyStore, boolean fetchMasterKey) {
90-
124+
91125
String ksAlias = dbKeyStore.getAlias();
92126

93127
String privateKeyObj = dbKeyStore.getPrivateKey();
@@ -105,21 +139,21 @@ public Object[] getKeyObjects(KeyStore dbKeyStore, boolean fetchMasterKey) {
105139
Certificate masterCert = masterKeyEntry.getCertificate();
106140
return new Object[] {masterPrivateKey, masterCert};
107141
}
108-
142+
109143
String masterKeyAlias = dbKeyStore.getMasterAlias();
110-
144+
111145
if (ksAlias.equals(masterKeyAlias) || privateKeyObj.equals(KeymanagerConstant.KS_PK_NA)) {
112146
LOGGER.error(KeymanagerConstant.SESSIONID, KeymanagerConstant.APPLICATIONID, null,
113147
"Not Allowed to perform decryption with other domain key.");
114148
throw new KeymanagerServiceException(KeymanagerErrorConstant.DECRYPTION_NOT_ALLOWED.getErrorCode(),
115149
KeymanagerErrorConstant.DECRYPTION_NOT_ALLOWED.getErrorMessage());
116150
}
117-
151+
118152
PrivateKeyEntry masterKeyEntry = keyStore.getAsymmetricKey(dbKeyStore.getMasterAlias());
119153
PrivateKey masterPrivateKey = masterKeyEntry.getPrivateKey();
120154
PublicKey masterPublicKey = masterKeyEntry.getCertificate().getPublicKey();
121155
try {
122-
byte[] decryptedPrivateKey = keymanagerUtil.decryptKey(CryptoUtil.decodeURLSafeBase64(dbKeyStore.getPrivateKey()),
156+
byte[] decryptedPrivateKey = keymanagerUtil.decryptKey(CryptoUtil.decodeURLSafeBase64(dbKeyStore.getPrivateKey()),
123157
masterPrivateKey, masterPublicKey);
124158
KeyFactory keyFactory = KeyFactory.getInstance(KeymanagerConstant.RSA);
125159
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decryptedPrivateKey));
@@ -131,5 +165,5 @@ public Object[] getKeyObjects(KeyStore dbKeyStore, boolean fetchMasterKey) {
131165
KeymanagerErrorConstant.CRYPTO_EXCEPTION.getErrorMessage() + e.getMessage(), e);
132166
}
133167
}
134-
168+
135169
}

kernel/kernel-keymanager-service/src/test/java/io/mosip/kernel/keymanagerservice/test/controller/KeymanagerControllerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ public void testGenerateMasterKeyWithCertificate() throws Exception {
462462
KeyPairGenerateRequestDto keyPairDto = new KeyPairGenerateRequestDto();
463463
keyPairDto.setApplicationId("TEST");
464464
keyPairDto.setReferenceId("");
465-
keyPairDto.setForce(true);
465+
keyPairDto.setForce(false);
466466
request.setRequest(keyPairDto);
467467

468468
mockMvc.perform(post("/generateMasterKey/CERTIFICATE")
Binary file not shown.

0 commit comments

Comments
 (0)