diff --git a/pom.xml b/pom.xml index c5855e0..cb25d59 100644 --- a/pom.xml +++ b/pom.xml @@ -121,6 +121,17 @@ spring-boot-starter-test test + + + + com.github.ben-manes.caffeine + caffeine + 3.0.5 + + + org.springframework.boot + spring-boot-starter-cache + diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/config/CacheConfig.java b/src/main/java/com/skycrate/backend/skycrateBackend/config/CacheConfig.java new file mode 100644 index 0000000..d1d828b --- /dev/null +++ b/src/main/java/com/skycrate/backend/skycrateBackend/config/CacheConfig.java @@ -0,0 +1,24 @@ +package com.skycrate.backend.skycrateBackend.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.caffeine.CaffeineCacheManager; + +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CaffeineCacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine(Caffeine.newBuilder() + .expireAfterWrite(30, TimeUnit.MINUTES) // Cache expiry time + .maximumSize(100)); // Maximum cache size + return cacheManager; + } +} + diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/config/SecurityConfig.java b/src/main/java/com/skycrate/backend/skycrateBackend/config/SecurityConfig.java index e0c6317..e5eaee2 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/config/SecurityConfig.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/config/SecurityConfig.java @@ -29,7 +29,7 @@ public class SecurityConfig { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationProvider(authenticationProvider) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/auth/login", "/api/auth/register", "/actuator/**").permitAll() + .requestMatchers("/api/auth/logout","/api/auth/login", "/api/auth/register", "/actuator/**").permitAll() .requestMatchers(HttpMethod.GET, "/public/**").permitAll() .anyRequest().authenticated() ) diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/controller/AuthController.java b/src/main/java/com/skycrate/backend/skycrateBackend/controller/AuthController.java index 6f11ada..4da9f45 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/controller/AuthController.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/controller/AuthController.java @@ -14,6 +14,8 @@ import com.skycrate.backend.skycrateBackend.services.JwtService; import com.skycrate.backend.skycrateBackend.services.RateLimiterService; import com.skycrate.backend.skycrateBackend.services.RefreshTokenService; import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -84,6 +86,23 @@ public class AuthController { return ResponseEntity.ok(new LoginResponse(accessToken, refreshToken.getToken())); } +// @PostMapping("/logout") +// public ResponseEntity logout(HttpServletRequest request) { +// String authHeader = request.getHeader("Authorization"); +// if (authHeader == null || !authHeader.startsWith("Bearer ")) { +// return ResponseEntity.badRequest().body("Missing or invalid Authorization header"); +// } +// +// String token = authHeader.substring(7); +// +// tokenBlacklistService.blacklistToken(token); +// +// String email = jwtService.extractUsername(token); +// userRepository.findByEmail(email).ifPresent(refreshTokenService::deleteByUser); +// +// return ResponseEntity.ok("Logged out successfully"); +// } + @PostMapping("/logout") public ResponseEntity logout(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); @@ -92,15 +111,38 @@ public class AuthController { } String token = authHeader.substring(7); + String username = jwtService.extractUsername(token); + + userRepository.findByUsername(username).ifPresent(user -> { + // Clear the cached decrypted private key for the user + authenticationService.clearDecryptedPrivateKeyCache(user.getId().toString()); + + // Delete the refresh token associated with the user + refreshTokenService.logout(user); // This should delete the token + }); tokenBlacklistService.blacklistToken(token); - String email = jwtService.extractUsername(token); - userRepository.findByEmail(email).ifPresent(refreshTokenService::deleteByUser); - return ResponseEntity.ok("Logged out successfully"); } +// @PostMapping("/refresh") +// public ResponseEntity refresh(@RequestBody TokenRefreshRequest request) { +// String requestToken = request.getRefreshToken(); +// +// return refreshTokenService.findByToken(requestToken) +// .map(token -> { +// if (refreshTokenService.isExpired(token)) { +// return ResponseEntity.status(403).body("Refresh token expired"); +// } +// +// User user = token.getUser(); +// String newAccessToken = jwtService.generateToken(user); +// return ResponseEntity.ok(new TokenRefreshResponse(newAccessToken, requestToken)); +// }) +// .orElseGet(() -> ResponseEntity.status(403).body("Invalid refresh token")); +// } + @PostMapping("/refresh") public ResponseEntity refresh(@RequestBody TokenRefreshRequest request) { String requestToken = request.getRefreshToken(); @@ -108,6 +150,8 @@ public class AuthController { return refreshTokenService.findByToken(requestToken) .map(token -> { if (refreshTokenService.isExpired(token)) { + // Clear the cached key on token expiry + authenticationService.clearDecryptedPrivateKeyCache(token.getUser().getId().toString()); return ResponseEntity.status(403).body("Refresh token expired"); } diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/repository/RefreshTokenRepository.java b/src/main/java/com/skycrate/backend/skycrateBackend/repository/RefreshTokenRepository.java index 0d825b7..a9c8a06 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/repository/RefreshTokenRepository.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/repository/RefreshTokenRepository.java @@ -15,4 +15,5 @@ public interface RefreshTokenRepository extends JpaRepository new RuntimeException("User not found")); } + + @Cacheable(value = "decryptedPrivateKeys", key = "#userId") + public byte[] getDecryptedPrivateKey(String userId, String password) throws Exception { + User user = userRepository.findById(Integer.valueOf(userId)) + .orElseThrow(() -> new RuntimeException("User not found: " + userId)); + + log.info("Caching decrypted private key for userId: {}", userId); + + SecretKey derivedKey = EncryptionUtil.deriveKey(password.toCharArray(), user.getPrivateKeySalt()); + byte[] decryptedPrivateKeyBytes = EncryptionUtil.decrypt(user.getPrivateKey(), derivedKey, user.getPrivateKeyIv()); + return decryptedPrivateKeyBytes; + } + + @CacheEvict(value = "decryptedPrivateKeys", key = "#userId") + public void clearDecryptedPrivateKeyCache(String userId) { + // This method will clear the cached decrypted private key for the given userId + log.info("Clearing Caching decrypted private key for userId: {}", userId); + keyCacheService.clearKey(Long.valueOf(userId)); + } } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/services/EncryptionUtil.java b/src/main/java/com/skycrate/backend/skycrateBackend/services/EncryptionUtil.java index aa10441..b9c1452 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/services/EncryptionUtil.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/EncryptionUtil.java @@ -17,63 +17,63 @@ public class EncryptionUtil { private static final int IV_LENGTH = 16; // for AES CBC private static final int ITERATIONS = 65536; private static final int KEY_LENGTH = 256; // bits -// -// // --- AES key derivation using PBKDF2 --- -// public static SecretKey deriveAESKey(char[] password, byte[] salt) -// throws NoSuchAlgorithmException, InvalidKeySpecException { -// -// SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); -// -// KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH); -// byte[] keyBytes = factory.generateSecret(spec).getEncoded(); -// -// return new SecretKeySpec(keyBytes, "AES"); -// } -// -// // --- Encrypt data using AES-CBC --- -// public static byte[] encrypt(byte[] data, SecretKey key, byte[] iv) -// throws GeneralSecurityException { -// -// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); -// -// IvParameterSpec ivSpec = new IvParameterSpec(iv); -// cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); -// -// return cipher.doFinal(data); -// } + + // --- AES key derivation using PBKDF2 --- + public static SecretKey deriveAESKey(char[] password, byte[] salt) + throws NoSuchAlgorithmException, InvalidKeySpecException { + + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + + KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH); + byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + + return new SecretKeySpec(keyBytes, "AES"); + } + + // --- Encrypt data using AES-CBC --- + public static byte[] encrypt(byte[] data, SecretKey key, byte[] iv) + throws GeneralSecurityException { + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + + return cipher.doFinal(data); + } // --- Decrypt data using AES-CBC --- -// public static byte[] decrypt(byte[] encryptedData, SecretKey key, byte[] iv) -// throws GeneralSecurityException { -// -// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); -// -// IvParameterSpec ivSpec = new IvParameterSpec(iv); -// cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); -// -// return cipher.doFinal(encryptedData); -// } -// -// // --- Generate random salt --- -// public static byte[] generateSalt() { -// byte[] salt = new byte[SALT_LENGTH]; -// new SecureRandom().nextBytes(salt); -// return salt; -// } + public static byte[] decrypt(byte[] encryptedData, SecretKey key, byte[] iv) + throws GeneralSecurityException { -// // --- Generate random IV --- -// public static byte[] generateIV() { -// byte[] iv = new byte[IV_LENGTH]; -// new SecureRandom().nextBytes(iv); -// return iv; -// } -// -// // --- Optional: Utility to base64 encode data --- -// public static String encodeBase64(byte[] data) { -// return Base64.getEncoder().encodeToString(data); -// } -// -// public static byte[] decodeBase64(String base64) { -// return Base64.getDecoder().decode(base64); -// } + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + + return cipher.doFinal(encryptedData); + } + + // --- Generate random salt --- + public static byte[] generateSalt() { + byte[] salt = new byte[SALT_LENGTH]; + new SecureRandom().nextBytes(salt); + return salt; + } + + // --- Generate random IV --- + public static byte[] generateIV() { + byte[] iv = new byte[IV_LENGTH]; + new SecureRandom().nextBytes(iv); + return iv; + } + + // --- Optional: Utility to base64 encode data --- + public static String encodeBase64(byte[] data) { + return Base64.getEncoder().encodeToString(data); + } + + public static byte[] decodeBase64(String base64) { + return Base64.getDecoder().decode(base64); + } } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/services/FileService.java b/src/main/java/com/skycrate/backend/skycrateBackend/services/FileService.java index fd1e1a2..24655bc 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/services/FileService.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/FileService.java @@ -13,7 +13,9 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import javax.crypto.SecretKey; import java.io.ByteArrayInputStream; @@ -24,15 +26,23 @@ import java.security.PublicKey; public class FileService { private static final Logger log = LoggerFactory.getLogger(FileService.class); - + private final AuthenticationService authenticationService; private final FileMetadataRepository fileMetadataRepository; private final UserRepository userRepository; - public FileService(FileMetadataRepository fileMetadataRepository, UserRepository userRepository) { +// public FileService(FileMetadataRepository fileMetadataRepository, UserRepository userRepository) { +// this.fileMetadataRepository = fileMetadataRepository; +// this.userRepository = userRepository; +// } + + @Autowired + public FileService(FileMetadataRepository fileMetadataRepository, UserRepository userRepository, AuthenticationService authenticationService) { this.fileMetadataRepository = fileMetadataRepository; this.userRepository = userRepository; + this.authenticationService = authenticationService; } + @Transactional public void uploadEncryptedFile(String username, byte[] fileContent, String filename) throws Exception { log.info("Starting upload for user={}, file={}", username, filename); try { @@ -81,6 +91,37 @@ public class FileService { } } +// public byte[] downloadDecryptedFile(String username, String password, String filename) throws Exception { +// log.info("Download request: user={}, file={}", username, filename); +// try { +// User user = userRepository.findByUsername(username) +// .orElseThrow(() -> new RuntimeException("User not found: " + username)); +// +// Path filePath = new Path("/" + username + "/" + filename); +// FileMetadata metadata = fileMetadataRepository.findByUsernameAndFilePath(username, filePath.toString()) +// .orElseThrow(() -> new RuntimeException("File metadata not found for: " + filePath)); +// +// SecretKey derivedKey = EncryptionUtil.deriveKey(password.toCharArray(), user.getPrivateKeySalt()); +// byte[] decryptedPrivateKeyBytes = EncryptionUtil.decrypt(user.getPrivateKey(), derivedKey, user.getPrivateKeyIv()); +// PrivateKey privateKey = RSAKeyUtil.decodePrivateKey(decryptedPrivateKeyBytes); +// +// byte[] aesKeyBytes = EncryptionUtil.decryptRSA(metadata.getEncryptedKey(), privateKey); +// SecretKey aesKey = EncryptionUtil.rebuildAESKey(aesKeyBytes); +// +// FileSystem fs = HDFSConfig.getHDFS(); +// byte[] encryptedData; +// try (FSDataInputStream in = fs.open(filePath)) { +// encryptedData = in.readAllBytes(); +// } +// +// return EncryptionUtil.decrypt(encryptedData, aesKey, metadata.getIv()); +// +// } catch (Exception e) { +// log.error("Download failed for user={}, file={}: {}", username, filename, e.getMessage(), e); +// throw e; +// } +// } + public byte[] downloadDecryptedFile(String username, String password, String filename) throws Exception { log.info("Download request: user={}, file={}", username, filename); try { @@ -91,8 +132,8 @@ public class FileService { FileMetadata metadata = fileMetadataRepository.findByUsernameAndFilePath(username, filePath.toString()) .orElseThrow(() -> new RuntimeException("File metadata not found for: " + filePath)); - SecretKey derivedKey = EncryptionUtil.deriveKey(password.toCharArray(), user.getPrivateKeySalt()); - byte[] decryptedPrivateKeyBytes = EncryptionUtil.decrypt(user.getPrivateKey(), derivedKey, user.getPrivateKeyIv()); + // Use the cached decrypted private key + byte[] decryptedPrivateKeyBytes = authenticationService.getDecryptedPrivateKey(String.valueOf(user.getId()), password); PrivateKey privateKey = RSAKeyUtil.decodePrivateKey(decryptedPrivateKeyBytes); byte[] aesKeyBytes = EncryptionUtil.decryptRSA(metadata.getEncryptedKey(), privateKey); diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/services/KeyCacheService.java b/src/main/java/com/skycrate/backend/skycrateBackend/services/KeyCacheService.java new file mode 100644 index 0000000..2727d82 --- /dev/null +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/KeyCacheService.java @@ -0,0 +1,28 @@ +package com.skycrate.backend.skycrateBackend.services; + +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class KeyCacheService { + + private final ConcurrentHashMap keyCache = new ConcurrentHashMap<>(); + + public void cacheKey(Long userId, String decryptedKey) { + keyCache.put(userId, decryptedKey); + } + + public String getKey(Long userId) { + return keyCache.get(userId); + } + + public void clearKey(Long userId) { + keyCache.remove(userId); + } + + public void clearAllKeys() { + keyCache.clear(); + } +} + diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/services/RefreshTokenService.java b/src/main/java/com/skycrate/backend/skycrateBackend/services/RefreshTokenService.java index b008b91..bedb9f5 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/services/RefreshTokenService.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/RefreshTokenService.java @@ -16,13 +16,25 @@ public class RefreshTokenService { private final RefreshTokenRepository refreshTokenRepo; - @Value("${security.jwt.refresh-expiry-ms:604800000}") // 7 days default + @Value("${security.jwt.refresh-expiry-ms:86400000}") //1 day in milliseconds private Long refreshTokenDurationMs; public RefreshTokenService(RefreshTokenRepository refreshTokenRepo) { this.refreshTokenRepo = refreshTokenRepo; } +// @Transactional +// public RefreshToken createRefreshToken(User user) { +// refreshTokenRepo.deleteByUser(user); +// refreshTokenRepo.flush(); +// +// RefreshToken token = new RefreshToken(); +// token.setUser(user); +// token.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs)); +// token.setToken(UUID.randomUUID().toString()); +// return refreshTokenRepo.save(token); +// } + @Transactional public RefreshToken createRefreshToken(User user) { refreshTokenRepo.deleteByUser(user); @@ -35,6 +47,7 @@ public class RefreshTokenService { return refreshTokenRepo.save(token); } + public Optional findByToken(String token) { return refreshTokenRepo.findByToken(token); } @@ -42,9 +55,28 @@ public class RefreshTokenService { public boolean isExpired(RefreshToken token) { return token.getExpiryDate().isBefore(Instant.now()); } +// +// @Transactional +// public void deleteByUser(User user) { +// refreshTokenRepo.deleteByUser(user); +// } @Transactional public void deleteByUser(User user) { - refreshTokenRepo.deleteByUser(user); + try { + refreshTokenRepo.deleteByUser(user); + System.out.println("Successfully deleted refresh tokens for user: " + user.getId()); + } catch (Exception e) { + System.err.println("Error deleting refresh tokens for user: " + user.getId() + " - " + e.getMessage()); + } + } + + @Transactional + public void logout(User user) { + deleteByUser(user); // This should call the repository method to delete the token + } + + public Optional refreshAccessToken(String refreshToken) { + return findByToken(refreshToken).filter(token -> !isExpired(token)); } } \ No newline at end of file