diff --git a/src/main/java/com/wultra/app/mobileutilityserver/config/CacheConfiguration.java b/src/main/java/com/wultra/app/mobileutilityserver/config/CacheConfiguration.java new file mode 100644 index 00000000..28d90d60 --- /dev/null +++ b/src/main/java/com/wultra/app/mobileutilityserver/config/CacheConfiguration.java @@ -0,0 +1,37 @@ +/* + * Wultra Mobile Utility Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.wultra.app.mobileutilityserver.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for caching. + * + * @author Petr Dvorak, petr@wultra.com + */ +@Configuration +@EnableCaching +public class CacheConfiguration { + + public static final String CERTIFICATE_FINGERPRINTS = "certificateFingerprints"; + public static final String MOBILE_APPS = "mobileApps"; + public static final String PRIVATE_KEYS = "privateKeys"; + +} diff --git a/src/main/java/com/wultra/app/mobileutilityserver/rest/filter/ResponseSignFilter.java b/src/main/java/com/wultra/app/mobileutilityserver/rest/filter/ResponseSignFilter.java index 45e096ef..c136f5eb 100644 --- a/src/main/java/com/wultra/app/mobileutilityserver/rest/filter/ResponseSignFilter.java +++ b/src/main/java/com/wultra/app/mobileutilityserver/rest/filter/ResponseSignFilter.java @@ -19,8 +19,8 @@ import com.wultra.app.mobileutilityserver.rest.http.HttpHeaders; import com.wultra.app.mobileutilityserver.rest.http.QueryParams; -import com.wultra.app.mobileutilityserver.rest.service.MobileAppService; import com.wultra.app.mobileutilityserver.rest.service.CryptographicOperationsService; +import com.wultra.app.mobileutilityserver.rest.service.MobileAppService; import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException; import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException; import org.springframework.beans.factory.annotation.Autowired; @@ -47,6 +47,8 @@ @Component public class ResponseSignFilter extends OncePerRequestFilter { + private static final String PATH_PREFIX = "/app/init"; + private final MobileAppService mobileAppService; private final CryptographicOperationsService cryptographicOperationsService; @@ -57,6 +59,12 @@ public ResponseSignFilter(MobileAppService mobileAppService, CryptographicOperat this.cryptographicOperationsService = cryptographicOperationsService; } + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + final String url = request.getRequestURI(); + return !url.startsWith(PATH_PREFIX); + } + @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain) throws ServletException, IOException { final ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); diff --git a/src/main/java/com/wultra/app/mobileutilityserver/rest/service/AdminService.java b/src/main/java/com/wultra/app/mobileutilityserver/rest/service/AdminService.java index 4b29083b..12941d3a 100644 --- a/src/main/java/com/wultra/app/mobileutilityserver/rest/service/AdminService.java +++ b/src/main/java/com/wultra/app/mobileutilityserver/rest/service/AdminService.java @@ -18,6 +18,7 @@ package com.wultra.app.mobileutilityserver.rest.service; +import com.wultra.app.mobileutilityserver.config.CacheConfiguration; import com.wultra.app.mobileutilityserver.database.model.CertificateEntity; import com.wultra.app.mobileutilityserver.database.model.MobileAppEntity; import com.wultra.app.mobileutilityserver.database.model.MobileDomainEntity; @@ -41,6 +42,8 @@ import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.openssl.PEMParser; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import javax.net.ssl.HttpsURLConnection; @@ -73,6 +76,8 @@ public class AdminService { private final CertificateConverter certificateConverter; private final MobileAppConverter mobileAppConverter; + private final CacheManager cacheManager; + private final CryptographicOperationsService cryptographicOperationsService; @Autowired @@ -80,12 +85,13 @@ public AdminService(MobileAppRepository mobileAppRepository, CertificateRepository certificateRepository, MobileDomainRepository mobileDomainRepository, CertificateConverter certificateConverter, - MobileAppConverter mobileAppConverter, CryptographicOperationsService cryptographicOperationsService) { + MobileAppConverter mobileAppConverter, CacheManager cacheManager, CryptographicOperationsService cryptographicOperationsService) { this.mobileAppRepository = mobileAppRepository; this.certificateRepository = certificateRepository; this.mobileDomainRepository = mobileDomainRepository; this.certificateConverter = certificateConverter; this.mobileAppConverter = mobileAppConverter; + this.cacheManager = cacheManager; this.cryptographicOperationsService = cryptographicOperationsService; } @@ -121,6 +127,8 @@ public ApplicationDetailResponse createApplication(CreateApplicationRequest requ final MobileAppEntity savedMobileAppEntity = mobileAppRepository.save(mobileAppEntity); + evictMobileAppAndPrivateKeyCache(name); + return mobileAppConverter.convertMobileApp(savedMobileAppEntity); } catch (CryptoProviderException e) { throw new AppException("Error while generating cryptographic keys", e); @@ -185,6 +193,8 @@ public CertificateDetailResponse createApplicationCertificate(String appName, Cr final CertificateEntity savedCertificateEntity = certificateRepository.save(certificateEntity); + evictCertificateFingerprintCache(appName); + final CertificateDetailResponse response = certificateConverter.convertCertificateDetailResponse(savedCertificateEntity); logger.info("Certificate refreshed: {}", response); return response; @@ -244,6 +254,7 @@ public void deleteCertificate(String appName, String domain, String fingerprint) if (certificate.getFingerprint().equalsIgnoreCase(fingerprint)) { certificates.remove(certificate); mobileDomainRepository.save(mobileDomainEntity); + evictCertificateFingerprintCache(appName); return; } } @@ -252,11 +263,49 @@ public void deleteCertificate(String appName, String domain, String fingerprint) @Transactional public void deleteDomain(String appName, String domain) { mobileDomainRepository.deleteByAppNameAndDomain(appName, domain); + evictCertificateFingerprintCache(appName); } @Transactional public void deleteExpiredCertificates() { certificateRepository.deleteAllByExpiresBefore(new Date().getTime() / 1000); + invalidateCertificateCache(); + } + + /** + * Evict caches for mobile apps and private keys for given app name. + * @param appName App Name. + */ + private void evictMobileAppAndPrivateKeyCache(String appName) { + final Cache mobileAppsCache = cacheManager.getCache(CacheConfiguration.MOBILE_APPS); + if (mobileAppsCache != null) { + mobileAppsCache.evict(appName); + } + final Cache privateKeyCache = cacheManager.getCache(CacheConfiguration.PRIVATE_KEYS); + if (privateKeyCache != null) { + privateKeyCache.evict(appName); + } + } + + /** + * Evict certificate fingerprint cache for a given app name. + * @param appName App name. + */ + private void evictCertificateFingerprintCache(String appName) { + final Cache cache = cacheManager.getCache(CacheConfiguration.CERTIFICATE_FINGERPRINTS); + if (cache != null) { + cache.evict(appName); + } + } + + /** + * Invalidate values in the certificate cache. + */ + private void invalidateCertificateCache() { + final Cache cache = cacheManager.getCache(CacheConfiguration.CERTIFICATE_FINGERPRINTS); + if (cache != null) { + cache.invalidate(); + } } } diff --git a/src/main/java/com/wultra/app/mobileutilityserver/rest/service/CertificateFingerprintService.java b/src/main/java/com/wultra/app/mobileutilityserver/rest/service/CertificateFingerprintService.java index 70d0df41..2753804f 100644 --- a/src/main/java/com/wultra/app/mobileutilityserver/rest/service/CertificateFingerprintService.java +++ b/src/main/java/com/wultra/app/mobileutilityserver/rest/service/CertificateFingerprintService.java @@ -18,11 +18,13 @@ package com.wultra.app.mobileutilityserver.rest.service; +import com.wultra.app.mobileutilityserver.config.CacheConfiguration; import com.wultra.app.mobileutilityserver.database.model.CertificateEntity; import com.wultra.app.mobileutilityserver.database.repo.CertificateRepository; import com.wultra.app.mobileutilityserver.rest.model.converter.CertificateConverter; import com.wultra.app.mobileutilityserver.rest.model.entity.CertificateFingerprint; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import javax.transaction.Transactional; @@ -54,6 +56,7 @@ public CertificateFingerprintService(CertificateRepository repo, CertificateConv * @return Collection with SSL pinning fingerprints, possibly empty. */ @Transactional + @Cacheable(cacheNames = CacheConfiguration.CERTIFICATE_FINGERPRINTS) public List findCertificateFingerprintsByAppName(String appName) { final List fingerprints = repo.findAllByAppName(appName); final List result = new ArrayList<>(); diff --git a/src/main/java/com/wultra/app/mobileutilityserver/rest/service/MobileAppService.java b/src/main/java/com/wultra/app/mobileutilityserver/rest/service/MobileAppService.java index 800fcfd7..0b6867f9 100644 --- a/src/main/java/com/wultra/app/mobileutilityserver/rest/service/MobileAppService.java +++ b/src/main/java/com/wultra/app/mobileutilityserver/rest/service/MobileAppService.java @@ -17,9 +17,11 @@ */ package com.wultra.app.mobileutilityserver.rest.service; +import com.wultra.app.mobileutilityserver.config.CacheConfiguration; import com.wultra.app.mobileutilityserver.database.model.MobileAppEntity; import com.wultra.app.mobileutilityserver.database.repo.MobileAppRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; /** @@ -42,6 +44,7 @@ public MobileAppService(MobileAppRepository repo) { * @param appName App name. * @return True in case the app with given name exists, false otherwise. */ + @Cacheable(cacheNames = CacheConfiguration.MOBILE_APPS) public boolean appExists(String appName) { return repo.existsByName(appName); } @@ -53,6 +56,7 @@ public boolean appExists(String appName) { * @return Private key encoded as Base64 representation of the embedded big integer, or null * if app with provided name does not exist. */ + @Cacheable(cacheNames = CacheConfiguration.PRIVATE_KEYS) public String privateKey(String appName) { final MobileAppEntity mobileAppEntity = repo.findFirstByName(appName); if (mobileAppEntity == null) {