Refactor file upload/download with service layer and secure encryption

- Replaced direct encryption logic in FileController with FileService delegation
- Added JWT-based username extraction in file operations
- Updated FileMetadata entity to include `uploadedAt` field and removed redundant getters/setters
- Refactored EncryptionUtil:
  - Switched to AES-CBC with PBKDF2 key derivation
  - Removed RSA-based encryption logic
  - Added salt and IV generation helpers
- Changed JwtAuthenticationFilter to fetch user by username (not email)
- Renamed method in FileMetadataRepository to match new parameter order

FILE UPLOAD NOW WORKS! TESTED USING CURL.
This commit is contained in:
K
2025-07-03 15:20:10 +05:30
parent f06dbd84ad
commit 23eda639c0
8 changed files with 255 additions and 152 deletions
@@ -1,79 +1,70 @@
package com.skycrate.backend.skycrateBackend.controller;
import com.skycrate.backend.skycrateBackend.entity.FileMetadata;
import com.skycrate.backend.skycrateBackend.repository.FileMetadataRepository;
import com.skycrate.backend.skycrateBackend.security.EncryptionService;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import com.skycrate.backend.skycrateBackend.services.FileService;
import com.skycrate.backend.skycrateBackend.services.JwtService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.crypto.SecretKey;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Optional;
@RestController
@RequestMapping("/api/files")
public class FileController {
@Autowired
private FileSystem hdfs;
private final FileService fileService;
private final JwtService jwtService;
@Autowired
private FileMetadataRepository metadataRepo;
public FileController(FileService fileService, JwtService jwtService) {
this.fileService = fileService;
this.jwtService = jwtService;
}
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file,
@RequestParam("password") String password,
Authentication auth) throws Exception {
public ResponseEntity<String> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam("password") String password,
HttpServletRequest request
) {
try {
String token = extractToken(request);
String username = jwtService.extractUsername(token);
byte[] fileBytes = file.getBytes();
byte[] salt = EncryptionService.generateSalt();
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
SecretKey key = EncryptionService.deriveKey(password, salt);
fileService.uploadEncryptedFile(username, password, file.getBytes(), file.getOriginalFilename());
byte[] encrypted = EncryptionService.encrypt(fileBytes, key, iv);
String pathStr = "/user/" + auth.getName() + "/" + file.getOriginalFilename();
Path hdfsPath = new Path(pathStr);
try (FSDataOutputStream out = hdfs.create(hdfsPath, true)) {
out.write(encrypted);
return ResponseEntity.ok("File uploaded and encrypted successfully.");
} catch (Exception e) {
return ResponseEntity.status(500).body("File upload failed: " + e.getMessage());
}
FileMetadata metadata = new FileMetadata();
metadata.setUsername(auth.getName());
metadata.setFilePath(pathStr);
metadata.setSalt(salt);
metadata.setIv(iv);
metadataRepo.save(metadata);
return "File uploaded and encrypted successfully!";
}
@GetMapping("/download")
public void download(@RequestParam("path") String path,
@RequestParam("password") String password,
Authentication auth,
OutputStream responseStream) throws Exception {
@GetMapping("/download/{filename}")
public ResponseEntity<?> downloadFile(
@PathVariable String filename,
@RequestParam("password") String password,
HttpServletRequest request
) {
try {
String token = extractToken(request);
String username = jwtService.extractUsername(token);
Optional<FileMetadata> optional = metadataRepo.findByFilePathAndUsername(path, auth.getName());
if (optional.isEmpty()) {
throw new SecurityException("You are not authorized to access this file.");
byte[] decryptedData = fileService.downloadDecryptedFile(username, password, filename);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(decryptedData);
} catch (Exception e) {
return ResponseEntity.status(500).body("File download failed: " + e.getMessage());
}
}
FileMetadata metadata = optional.get();
SecretKey key = EncryptionService.deriveKey(password, metadata.getSalt());
try (FSDataInputStream input = hdfs.open(new Path(path))) {
byte[] encrypted = input.readAllBytes();
byte[] decrypted = EncryptionService.decrypt(encrypted, key, metadata.getIv());
responseStream.write(decrypted);
private String extractToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("Missing or invalid Authorization header");
}
return authHeader.substring(7);
}
}
@@ -30,14 +30,7 @@ public class FileMetadata {
@Column(nullable = false)
private byte[] iv;
public void setUsername(String username) { this.username = username; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public void setSalt(byte[] salt) { this.salt = salt; }
public void setIv(byte[] iv) { this.iv = iv; }
public String getUsername() { return this.username; }
public String getFilePath() { return this.filePath; }
public byte[] getSalt() { return this.salt; }
public byte[] getIv() { return this.iv; }
@Column(nullable = false)
private long uploadedAt;
}
@@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface FileMetadataRepository extends JpaRepository<FileMetadata, Long> {
Optional<FileMetadata> findByFilePathAndUsername(String filePath, String username);
Optional<FileMetadata> findByUsernameAndFilePath(String username, String filePath);
}
@@ -45,7 +45,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
final String username;
if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
@@ -62,15 +62,16 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
}
try {
userEmail = jwtService.extractUsername(jwt);
username = jwtService.extractUsername(jwt); // This is actually the `username`, not email
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid JWT token");
return;
}
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
User user = userRepository.findByEmail(userEmail).orElse(null);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// ❗ Use username to find the user
User user = userRepository.findByUsername(username).orElse(null);
if (user != null && jwtService.isTokenValid(jwt, user)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
@@ -1,102 +1,79 @@
package com.skycrate.backend.skycrateBackend.services;
import com.skycrate.backend.skycrateBackend.utils.RSAKeyUtil;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;
public class EncryptionUtil {
private static final String RSA_ALGORITHM = "RSA";
private static final String AES_ALGORITHM = "AES";
private static final int RSA_KEY_SIZE = 2048;
private static final int AES_KEY_SIZE = 256;
// Generate RSA Key Pair (Public & Private)
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA_ALGORITHM);
keyGen.initialize(RSA_KEY_SIZE);
return keyGen.generateKeyPair();
private static final int SALT_LENGTH = 16; // in bytes
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 (AES Key is encrypted using RSA)
// public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
// // Step 1: Generate AES Key
// SecretKey aesKey = generateAESKey();
//
// // Encrypt data using AES
// Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
// aesCipher.init(Cipher.ENCRYPT_MODE, aesKey);
// byte[] encryptedData = aesCipher.doFinal(data);
//
// // Encrypt the AES key with RSA
// Cipher rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
// rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
// byte[] encryptedAesKey = rsaCipher.doFinal(aesKey.getEncoded());
//
// // Step 4: Combine encrypted AES key and encrypted data into one array
// byte[] combined = new byte[4 + encryptedAesKey.length + encryptedData.length];
//
// // First 4 bytes indicate the length of the AES encrypted key
// combined[0] = (byte) (encryptedAesKey.length >> 24);
// combined[1] = (byte) (encryptedAesKey.length >> 16);
// combined[2] = (byte) (encryptedAesKey.length >> 8);
// combined[3] = (byte) encryptedAesKey.length;
//
// // Copy AES Key and Encrypted Data into the combined array
// System.arraycopy(encryptedAesKey, 0, combined, 4, encryptedAesKey.length);
// System.arraycopy(encryptedData, 0, combined, 4 + encryptedAesKey.length, encryptedData.length);
//
// return combined;
// }
// --- Encrypt data using AES-CBC ---
public static byte[] encrypt(byte[] data, SecretKey key, byte[] iv)
throws GeneralSecurityException {
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
SecretKey aesKey = RSAKeyUtil.generateAESKey(256); // Ensure 256 bits
byte[] encryptedData = encryptDataWithAES(data, aesKey);
byte[] encryptedAesKey = RSAKeyUtil.encryptAESKey(aesKey, publicKey);
return combineEncryptedData(encryptedAesKey, encryptedData);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
return cipher.doFinal(data);
}
private static byte[] encryptDataWithAES(byte[] data, SecretKey aesKey) throws Exception {
Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey);
return aesCipher.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);
}
private static byte[] combineEncryptedData(byte[] encryptedAesKey, byte[] encryptedData) {
byte[] combined = new byte[4 + encryptedAesKey.length + encryptedData.length];
System.arraycopy(encryptedAesKey, 0, combined, 4, encryptedAesKey.length);
System.arraycopy(encryptedData, 0, combined, 4 + encryptedAesKey.length, encryptedData.length);
return combined;
// --- 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;
}
// Decrypt data using RSA (AES Key is decrypted using RSA, then used for AES decryption)
public static byte[] decrypt(byte[] encryptedCombined, PrivateKey privateKey) throws Exception {
// Step 1: Extract AES Key length from the combined data
int aesKeyLength = ((encryptedCombined[0] & 0xFF) << 24) |
((encryptedCombined[1] & 0xFF) << 16) |
((encryptedCombined[2] & 0xFF) << 8) |
(encryptedCombined[3] & 0xFF);
// --- Optional: Utility to base64 encode data ---
public static String encodeBase64(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
// Step 2: Extract the encrypted AES key and encrypted data
byte[] encryptedAesKey = new byte[aesKeyLength];
byte[] encryptedData = new byte[encryptedCombined.length - 4 - aesKeyLength];
System.arraycopy(encryptedCombined, 4, encryptedAesKey, 0, aesKeyLength);
System.arraycopy(encryptedCombined, 4 + aesKeyLength, encryptedData, 0, encryptedData.length);
// Step 3: Decrypt the AES key using RSA
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] aesKeyBytes = rsaCipher.doFinal(encryptedAesKey);
// Create AES key
SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
// Decrypt the data using AES
Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
aesCipher.init(Cipher.DECRYPT_MODE, aesKey);
return aesCipher.doFinal(encryptedData);
public static byte[] decodeBase64(String base64) {
return Base64.getDecoder().decode(base64);
}
}
@@ -0,0 +1,87 @@
package com.skycrate.backend.skycrateBackend.services;
import com.skycrate.backend.skycrateBackend.config.HDFSConfig;
import com.skycrate.backend.skycrateBackend.entity.FileMetadata;
import com.skycrate.backend.skycrateBackend.repository.FileMetadataRepository;
import com.skycrate.backend.skycrateBackend.utils.EncryptionUtil;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Optional;
@Service
public class FileService {
private final FileMetadataRepository fileMetadataRepository;
public FileService(FileMetadataRepository fileMetadataRepository) {
this.fileMetadataRepository = fileMetadataRepository;
}
public void uploadEncryptedFile(String username, String password, byte[] fileContent, String filename) throws Exception {
// Generate salt and IV
byte[] salt = EncryptionUtil.generateSalt();
byte[] iv = EncryptionUtil.generateIv();
// Derive AES key
SecretKey key = EncryptionUtil.deriveKey(password.toCharArray(), salt);
// Encrypt file content
byte[] encryptedData = EncryptionUtil.encrypt(fileContent, key, iv);
// Prepare HDFS path
Path userDir = new Path("/" + username);
Path filePath = new Path(userDir, filename);
FileSystem fs = HDFSConfig.getHDFS();
// Ensure user directory exists
if (!fs.exists(userDir)) {
fs.mkdirs(userDir);
}
// Write encrypted file to HDFS
try (FSDataOutputStream outputStream = fs.create(filePath, true);
InputStream in = new ByteArrayInputStream(encryptedData)) {
in.transferTo(outputStream);
}
// Save metadata
FileMetadata metadata = FileMetadata.builder()
.username(username)
.filePath(filePath.toString())
.salt(salt)
.iv(iv)
.build();
fileMetadataRepository.save(metadata);
}
public byte[] downloadDecryptedFile(String username, String password, String filename) throws Exception {
Path filePath = new Path("/" + username + "/" + filename);
FileSystem fs = HDFSConfig.getHDFS();
Optional<FileMetadata> metadataOpt = fileMetadataRepository.findByUsernameAndFilePath(username, filePath.toString());
if (metadataOpt.isEmpty()) {
throw new RuntimeException("File metadata not found");
}
FileMetadata metadata = metadataOpt.get();
// Derive key
SecretKey key = EncryptionUtil.deriveKey(password.toCharArray(), metadata.getSalt());
// Read file from HDFS
byte[] encryptedData = Files.readAllBytes(
new java.io.File(filePath.toString()).toPath()
);
// Decrypt
return EncryptionUtil.decrypt(encryptedData, key, metadata.getIv());
}
}
@@ -0,0 +1,54 @@
package com.skycrate.backend.skycrateBackend.utils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.*;
import java.security.SecureRandom;
import java.util.Arrays;
public class EncryptionUtil {
private static final int SALT_LENGTH = 16; // 128 bits
private static final int IV_LENGTH = 16; // 128 bits for AES
private static final int ITERATIONS = 65536;
private static final int KEY_LENGTH = 256; // AES-256
private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";
private static final String AES_CIPHER = "AES/CBC/PKCS5Padding";
private static final SecureRandom secureRandom = new SecureRandom();
public static byte[] generateSalt() {
byte[] salt = new byte[SALT_LENGTH];
secureRandom.nextBytes(salt);
return salt;
}
public static byte[] generateIv() {
byte[] iv = new byte[IV_LENGTH];
secureRandom.nextBytes(iv);
return iv;
}
public static SecretKey deriveKey(char[] password, byte[] salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), "AES");
}
public static byte[] encrypt(byte[] plaintext, SecretKey key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
return cipher.doFinal(plaintext);
}
public static byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
return cipher.doFinal(ciphertext);
}
}