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,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());
}
}
@@ -63,4 +63,4 @@ public class JwtService {
public String generateToken(User user) {
return generateToken((UserDetails) user);
}
}
}