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; package com.skycrate.backend.skycrateBackend.controller;
import com.skycrate.backend.skycrateBackend.entity.FileMetadata; import com.skycrate.backend.skycrateBackend.services.FileService;
import com.skycrate.backend.skycrateBackend.repository.FileMetadataRepository; import com.skycrate.backend.skycrateBackend.services.JwtService;
import com.skycrate.backend.skycrateBackend.security.EncryptionService; import jakarta.servlet.http.HttpServletRequest;
import org.apache.hadoop.fs.FSDataInputStream; import org.springframework.http.HttpHeaders;
import org.apache.hadoop.fs.FSDataOutputStream; import org.springframework.http.MediaType;
import org.apache.hadoop.fs.FileSystem; import org.springframework.http.ResponseEntity;
import org.apache.hadoop.fs.Path;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.crypto.SecretKey;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Optional;
@RestController @RestController
@RequestMapping("/api/files") @RequestMapping("/api/files")
public class FileController { public class FileController {
@Autowired private final FileService fileService;
private FileSystem hdfs; private final JwtService jwtService;
@Autowired public FileController(FileService fileService, JwtService jwtService) {
private FileMetadataRepository metadataRepo; this.fileService = fileService;
this.jwtService = jwtService;
}
@PostMapping("/upload") @PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file, public ResponseEntity<String> uploadFile(
@RequestParam("password") String password, @RequestParam("file") MultipartFile file,
Authentication auth) throws Exception { @RequestParam("password") String password,
HttpServletRequest request
) {
try {
String token = extractToken(request);
String username = jwtService.extractUsername(token);
byte[] fileBytes = file.getBytes(); fileService.uploadEncryptedFile(username, password, file.getBytes(), file.getOriginalFilename());
byte[] salt = EncryptionService.generateSalt();
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
SecretKey key = EncryptionService.deriveKey(password, salt);
byte[] encrypted = EncryptionService.encrypt(fileBytes, key, iv); return ResponseEntity.ok("File uploaded and encrypted successfully.");
String pathStr = "/user/" + auth.getName() + "/" + file.getOriginalFilename(); } catch (Exception e) {
Path hdfsPath = new Path(pathStr); return ResponseEntity.status(500).body("File upload failed: " + e.getMessage());
try (FSDataOutputStream out = hdfs.create(hdfsPath, true)) {
out.write(encrypted);
} }
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") @GetMapping("/download/{filename}")
public void download(@RequestParam("path") String path, public ResponseEntity<?> downloadFile(
@RequestParam("password") String password, @PathVariable String filename,
Authentication auth, @RequestParam("password") String password,
OutputStream responseStream) throws Exception { HttpServletRequest request
) {
try {
String token = extractToken(request);
String username = jwtService.extractUsername(token);
Optional<FileMetadata> optional = metadataRepo.findByFilePathAndUsername(path, auth.getName()); byte[] decryptedData = fileService.downloadDecryptedFile(username, password, filename);
if (optional.isEmpty()) {
throw new SecurityException("You are not authorized to access this file."); 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(); private String extractToken(HttpServletRequest request) {
SecretKey key = EncryptionService.deriveKey(password, metadata.getSalt()); String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
try (FSDataInputStream input = hdfs.open(new Path(path))) { throw new RuntimeException("Missing or invalid Authorization header");
byte[] encrypted = input.readAllBytes();
byte[] decrypted = EncryptionService.decrypt(encrypted, key, metadata.getIv());
responseStream.write(decrypted);
} }
return authHeader.substring(7);
} }
} }
@@ -30,14 +30,7 @@ public class FileMetadata {
@Column(nullable = false) @Column(nullable = false)
private byte[] iv; private byte[] iv;
public void setUsername(String username) { this.username = username; } @Column(nullable = false)
public void setFilePath(String filePath) { this.filePath = filePath; } private long uploadedAt;
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; }
} }
@@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional; import java.util.Optional;
public interface FileMetadataRepository extends JpaRepository<FileMetadata, Long> { 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 authHeader = request.getHeader("Authorization");
final String jwt; final String jwt;
final String userEmail; final String username;
if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) { if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
@@ -62,15 +62,16 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
} }
try { try {
userEmail = jwtService.extractUsername(jwt); username = jwtService.extractUsername(jwt); // This is actually the `username`, not email
} catch (Exception e) { } catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid JWT token"); response.getWriter().write("Invalid JWT token");
return; return;
} }
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
User user = userRepository.findByEmail(userEmail).orElse(null); // ❗ Use username to find the user
User user = userRepository.findByUsername(username).orElse(null);
if (user != null && jwtService.isTokenValid(jwt, user)) { if (user != null && jwtService.isTokenValid(jwt, user)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
@@ -1,102 +1,79 @@
package com.skycrate.backend.skycrateBackend.services; package com.skycrate.backend.skycrateBackend.services;
import com.skycrate.backend.skycrateBackend.utils.RSAKeyUtil;
import javax.crypto.*; import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*; import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;
public class EncryptionUtil { 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) private static final int SALT_LENGTH = 16; // in bytes
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { private static final int IV_LENGTH = 16; // for AES CBC
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA_ALGORITHM); private static final int ITERATIONS = 65536;
keyGen.initialize(RSA_KEY_SIZE); private static final int KEY_LENGTH = 256; // bits
return keyGen.generateKeyPair();
// --- 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) // --- Encrypt data using AES-CBC ---
// public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { public static byte[] encrypt(byte[] data, SecretKey key, byte[] iv)
// // Step 1: Generate AES Key throws GeneralSecurityException {
// 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;
// }
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKey aesKey = RSAKeyUtil.generateAESKey(256); // Ensure 256 bits
byte[] encryptedData = encryptDataWithAES(data, aesKey); IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] encryptedAesKey = RSAKeyUtil.encryptAESKey(aesKey, publicKey); cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
return combineEncryptedData(encryptedAesKey, encryptedData);
return cipher.doFinal(data);
} }
private static byte[] encryptDataWithAES(byte[] data, SecretKey aesKey) throws Exception { // --- Decrypt data using AES-CBC ---
Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); public static byte[] decrypt(byte[] encryptedData, SecretKey key, byte[] iv)
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey); throws GeneralSecurityException {
return aesCipher.doFinal(data);
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) { // --- Generate random salt ---
byte[] combined = new byte[4 + encryptedAesKey.length + encryptedData.length]; public static byte[] generateSalt() {
System.arraycopy(encryptedAesKey, 0, combined, 4, encryptedAesKey.length); byte[] salt = new byte[SALT_LENGTH];
System.arraycopy(encryptedData, 0, combined, 4 + encryptedAesKey.length, encryptedData.length); new SecureRandom().nextBytes(salt);
return combined; 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) // --- Optional: Utility to base64 encode data ---
public static byte[] decrypt(byte[] encryptedCombined, PrivateKey privateKey) throws Exception { public static String encodeBase64(byte[] data) {
// Step 1: Extract AES Key length from the combined data return Base64.getEncoder().encodeToString(data);
int aesKeyLength = ((encryptedCombined[0] & 0xFF) << 24) | }
((encryptedCombined[1] & 0xFF) << 16) |
((encryptedCombined[2] & 0xFF) << 8) |
(encryptedCombined[3] & 0xFF);
// Step 2: Extract the encrypted AES key and encrypted data public static byte[] decodeBase64(String base64) {
byte[] encryptedAesKey = new byte[aesKeyLength]; return Base64.getDecoder().decode(base64);
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);
} }
} }
@@ -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());
}
}
@@ -63,4 +63,4 @@ public class JwtService {
public String generateToken(User user) { public String generateToken(User user) {
return generateToken((UserDetails) user); return generateToken((UserDetails) user);
} }
} }
@@ -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);
}
}