diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/controller/FileController.java b/src/main/java/com/skycrate/backend/skycrateBackend/controller/FileController.java index 3e7bfd6..e84b596 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/controller/FileController.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/controller/FileController.java @@ -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 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 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); } } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/entity/FileMetadata.java b/src/main/java/com/skycrate/backend/skycrateBackend/entity/FileMetadata.java index a077276..fdd60e0 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/entity/FileMetadata.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/entity/FileMetadata.java @@ -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; } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/repository/FileMetadataRepository.java b/src/main/java/com/skycrate/backend/skycrateBackend/repository/FileMetadataRepository.java index f4bab67..758d250 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/repository/FileMetadataRepository.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/repository/FileMetadataRepository.java @@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface FileMetadataRepository extends JpaRepository { - Optional findByFilePathAndUsername(String filePath, String username); + Optional findByUsernameAndFilePath(String username, String filePath); } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/security/JwtAuthenticationFilter.java b/src/main/java/com/skycrate/backend/skycrateBackend/security/JwtAuthenticationFilter.java index 38b9e37..f42b953 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/security/JwtAuthenticationFilter.java @@ -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( 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 61b41de..b9c1452 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/services/EncryptionUtil.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/EncryptionUtil.java @@ -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); } } \ 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 new file mode 100644 index 0000000..cd5c0f1 --- /dev/null +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/FileService.java @@ -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 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()); + } +} \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/services/JwtService.java b/src/main/java/com/skycrate/backend/skycrateBackend/services/JwtService.java index fd644ed..1a5a39b 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/services/JwtService.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/JwtService.java @@ -63,4 +63,4 @@ public class JwtService { public String generateToken(User user) { return generateToken((UserDetails) user); } -} +} \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/utils/EncryptionUtil.java b/src/main/java/com/skycrate/backend/skycrateBackend/utils/EncryptionUtil.java new file mode 100644 index 0000000..e5345a0 --- /dev/null +++ b/src/main/java/com/skycrate/backend/skycrateBackend/utils/EncryptionUtil.java @@ -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); + } +} \ No newline at end of file