From 4af5aabd4245c918ab31dfcb8cfde39e3c478ebd Mon Sep 17 00:00:00 2001 From: Kshitij <160704796+kshitij-ka@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:22:41 +0530 Subject: [PATCH] Refactor encryption system to support hybrid RSA-AES encryption per file - Changed file upload logic to: - Generate random AES key per file - Encrypt AES key using user's RSA public key - Store encrypted AES key, IV, and salt in FileMetadata entity - Changed file download logic to: - Decrypt AES key using user's RSA private key (encrypted with password-derived AES) - Use decrypted AES key and IV to decrypt file contents from HDFS - Modified FileMetadata entity: - Changed `encryptedKey` to @Lob byte[] to support large encrypted AES keys - Updated User entity: - Encrypted private RSA key with password-derived AES - Stored associated salt and IV for decryption - Updated AuthenticationService: - Generate RSA keypair during sign-up - Encrypt and store private key with AES (salt, IV) - Create user folder in HDFS upon registration - Updated FileService: - Rewrote upload and download logic to support hybrid encryption - Handled key wrapping and unwrapping securely - Added logging for upload/download events - Fixed FileController upload to remove password from endpoint - Password now only required during download for private key decryption - Updated EncryptionUtil and RSAKeyUtil: - Added RSA OAEP support and helper methods - Added AES key generation, encryption, decryption utilities FILE UPLOAD AND ENCRYPTION WORKS! TESTED USING HEXDUMP. --- .../controller/FileController.java | 12 +- .../skycrateBackend/entity/FileMetadata.java | 5 +- .../backend/skycrateBackend/entity/User.java | 34 +++-- .../services/AuthenticationService.java | 24 +++- .../skycrateBackend/services/FileService.java | 135 +++++++++++------- .../skycrateBackend/utils/EncryptionUtil.java | 50 ++++++- .../skycrateBackend/utils/RSAKeyUtil.java | 35 ++--- 7 files changed, 190 insertions(+), 105 deletions(-) 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 e84b596..e26a354 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/controller/FileController.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/controller/FileController.java @@ -4,6 +4,7 @@ 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.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -22,20 +23,19 @@ public class FileController { } @PostMapping("/upload") - public ResponseEntity uploadFile( + public ResponseEntity uploadFile( @RequestParam("file") MultipartFile file, - @RequestParam("password") String password, - HttpServletRequest request - ) { + HttpServletRequest request) { try { String token = extractToken(request); String username = jwtService.extractUsername(token); - fileService.uploadEncryptedFile(username, password, file.getBytes(), file.getOriginalFilename()); + fileService.uploadEncryptedFile(username, file.getBytes(), file.getOriginalFilename()); return ResponseEntity.ok("File uploaded and encrypted successfully."); } catch (Exception e) { - return ResponseEntity.status(500).body("File upload failed: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Upload failed: " + e.getMessage()); } } 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 fdd60e0..06f0d09 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/entity/FileMetadata.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/entity/FileMetadata.java @@ -30,7 +30,10 @@ public class FileMetadata { @Column(nullable = false) private byte[] iv; + @Lob + @Column(nullable = false, name = "encrypted_key") + private byte[] encryptedKey; + @Column(nullable = false) private long uploadedAt; - } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/entity/User.java b/src/main/java/com/skycrate/backend/skycrateBackend/entity/User.java index 78fc9ef..945ae03 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/entity/User.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/entity/User.java @@ -38,25 +38,37 @@ public class User implements UserDetails { @Lob private byte[] privateKey; + @Lob + @Column(nullable = false) + private byte[] privateKeySalt; + + @Lob + @Column(nullable = false) + private byte[] privateKeyIv; + @Builder - public User(String email, String password, String username, String fullname, byte[] publicKey, byte[] privateKey) { + public User(String email, String password, String username, String fullname, + byte[] publicKey, byte[] privateKey, + byte[] privateKeySalt, byte[] privateKeyIv) { this.email = email; this.password = password; this.username = username; this.fullname = fullname; this.publicKey = publicKey; this.privateKey = privateKey; + this.privateKeySalt = privateKeySalt; + this.privateKeyIv = privateKeyIv; } // --- UserDetails interface methods --- @Override public Collection getAuthorities() { - return List.of(); // Add roles/authorities if needed + return List.of(); // No roles assigned currently } @Override public String getUsername() { - return username; // or return username if that's your login key + return username; } @Override @@ -65,22 +77,14 @@ public class User implements UserDetails { } @Override - public boolean isAccountNonExpired() { - return true; - } + public boolean isAccountNonExpired() { return true; } @Override - public boolean isAccountNonLocked() { - return true; - } + public boolean isAccountNonLocked() { return true; } @Override - public boolean isCredentialsNonExpired() { - return true; - } + public boolean isCredentialsNonExpired() { return true; } @Override - public boolean isEnabled() { - return true; - } + public boolean isEnabled() { return true; } } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/services/AuthenticationService.java b/src/main/java/com/skycrate/backend/skycrateBackend/services/AuthenticationService.java index 2f49b40..e8e126c 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/services/AuthenticationService.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/AuthenticationService.java @@ -5,6 +5,7 @@ import com.skycrate.backend.skycrateBackend.dto.LoginUserDto; import com.skycrate.backend.skycrateBackend.dto.RegisterUserDto; import com.skycrate.backend.skycrateBackend.entity.User; import com.skycrate.backend.skycrateBackend.repository.UserRepository; +import com.skycrate.backend.skycrateBackend.utils.EncryptionUtil; import com.skycrate.backend.skycrateBackend.utils.RSAKeyUtil; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; @@ -13,6 +14,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import javax.crypto.SecretKey; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; @@ -32,6 +34,7 @@ public class AuthenticationService { } public User signUp(RegisterUserDto inputUser) { + // Generate RSA key pair KeyPair keyPair; try { keyPair = RSAKeyUtil.generateKeyPair(); @@ -39,28 +42,43 @@ public class AuthenticationService { throw new RuntimeException("Failed to generate RSA key pair", e); } + // Encrypt private key using password-derived AES key + byte[] salt = EncryptionUtil.generateSalt(); + byte[] iv = EncryptionUtil.generateIv(); + byte[] encryptedPrivateKey; + try { + SecretKey aesKey = EncryptionUtil.deriveKey(inputUser.getPassword().toCharArray(), salt); + encryptedPrivateKey = EncryptionUtil.encrypt(keyPair.getPrivate().getEncoded(), aesKey, iv); + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt private key", e); + } + + // Create user entity with encrypted private key, salt, and iv User user = User.builder() .fullname(inputUser.getFirstname() + " " + inputUser.getLastname()) .username(inputUser.getUsername()) .email(inputUser.getEmail()) .password(passwordEncoder.encode(inputUser.getPassword())) .publicKey(keyPair.getPublic().getEncoded()) - .privateKey(keyPair.getPrivate().getEncoded()) + .privateKey(encryptedPrivateKey) + .privateKeySalt(salt) + .privateKeyIv(iv) .build(); + // Save user User savedUser = userRepository.save(user); // Create HDFS directory in root with username try { FileSystem fs = HDFSConfig.getHDFS(); - String folderName = savedUser.getUsername(); - Path userDir = new Path("/" + folderName); + Path userDir = new Path("/" + savedUser.getUsername()); if (!fs.exists(userDir)) { fs.mkdirs(userDir); } } catch (Exception e) { throw new RuntimeException("Failed to create HDFS directory for user: " + savedUser.getUsername(), e); } + return savedUser; } 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 cd5c0f1..fd1e1a2 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/services/FileService.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/services/FileService.java @@ -2,86 +2,113 @@ 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.entity.User; import com.skycrate.backend.skycrateBackend.repository.FileMetadataRepository; +import com.skycrate.backend.skycrateBackend.repository.UserRepository; import com.skycrate.backend.skycrateBackend.utils.EncryptionUtil; +import com.skycrate.backend.skycrateBackend.utils.RSAKeyUtil; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; 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; +import java.security.PrivateKey; +import java.security.PublicKey; @Service public class FileService { - private final FileMetadataRepository fileMetadataRepository; + private static final Logger log = LoggerFactory.getLogger(FileService.class); - public FileService(FileMetadataRepository fileMetadataRepository) { + private final FileMetadataRepository fileMetadataRepository; + private final UserRepository userRepository; + + public FileService(FileMetadataRepository fileMetadataRepository, UserRepository userRepository) { this.fileMetadataRepository = fileMetadataRepository; + this.userRepository = userRepository; } - 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(); + public void uploadEncryptedFile(String username, byte[] fileContent, String filename) throws Exception { + log.info("Starting upload for user={}, file={}", username, filename); + try { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); - // Derive AES key - SecretKey key = EncryptionUtil.deriveKey(password.toCharArray(), salt); + SecretKey aesKey = EncryptionUtil.generateAESKey(); + byte[] salt = EncryptionUtil.generateSalt(); // reserved for future use + byte[] iv = EncryptionUtil.generateIv(); - // Encrypt file content - byte[] encryptedData = EncryptionUtil.encrypt(fileContent, key, iv); + byte[] encryptedData = EncryptionUtil.encrypt(fileContent, aesKey, iv); - // Prepare HDFS path - Path userDir = new Path("/" + username); - Path filePath = new Path(userDir, filename); - FileSystem fs = HDFSConfig.getHDFS(); + PublicKey publicKey = RSAKeyUtil.decodePublicKey(user.getPublicKey()); + byte[] encryptedAesKey = EncryptionUtil.encryptRSA(aesKey.getEncoded(), publicKey); - // Ensure user directory exists - if (!fs.exists(userDir)) { - fs.mkdirs(userDir); + Path userDir = new Path("/" + username); + Path filePath = new Path(userDir, filename); + FileSystem fs = HDFSConfig.getHDFS(); + + if (!fs.exists(userDir)) { + log.info("Creating directory in HDFS: {}", userDir); + fs.mkdirs(userDir); + } + + log.info("Writing encrypted file to HDFS: {}", filePath); + try (FSDataOutputStream out = fs.create(filePath, true); + ByteArrayInputStream in = new ByteArrayInputStream(encryptedData)) { + in.transferTo(out); + } + + FileMetadata metadata = FileMetadata.builder() + .username(username) + .filePath(filePath.toString()) + .salt(salt) + .iv(iv) + .encryptedKey(encryptedAesKey) + .uploadedAt(System.currentTimeMillis()) + .build(); + + fileMetadataRepository.save(metadata); + log.info("Upload complete: file={} for user={}", filename, username); + + } catch (Exception e) { + log.error("Error during file upload for user={}, file={}: {}", username, filename, e.getMessage(), e); + throw e; } - - // 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(); + log.info("Download request: user={}, file={}", username, filename); + try { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); - Optional metadataOpt = fileMetadataRepository.findByUsernameAndFilePath(username, filePath.toString()); - if (metadataOpt.isEmpty()) { - throw new RuntimeException("File metadata not found"); + 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; } - - 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/utils/EncryptionUtil.java b/src/main/java/com/skycrate/backend/skycrateBackend/utils/EncryptionUtil.java index e5345a0..d5ad1ea 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/utils/EncryptionUtil.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/utils/EncryptionUtil.java @@ -4,21 +4,25 @@ import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.*; -import java.security.SecureRandom; +import java.security.*; +import javax.crypto.spec.PBEKeySpec; 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 SALT_LENGTH = 16; + private static final int IV_LENGTH = 16; private static final int ITERATIONS = 65536; - private static final int KEY_LENGTH = 256; // AES-256 + private static final int KEY_LENGTH = 256; - private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256"; + private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; private static final String AES_CIPHER = "AES/CBC/PKCS5Padding"; + private static final String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; private static final SecureRandom secureRandom = new SecureRandom(); + // ------------------------ AES ------------------------ + public static byte[] generateSalt() { byte[] salt = new byte[SALT_LENGTH]; secureRandom.nextBytes(salt); @@ -38,6 +42,16 @@ public class EncryptionUtil { return new SecretKeySpec(tmp.getEncoded(), "AES"); } + public static SecretKey generateAESKey() throws Exception { + byte[] keyBytes = new byte[KEY_LENGTH / 8]; // 256 bits + secureRandom.nextBytes(keyBytes); + return new SecretKeySpec(keyBytes, "AES"); + } + + public static SecretKey rebuildAESKey(byte[] keyBytes) { + return new SecretKeySpec(keyBytes, "AES"); + } + public static byte[] encrypt(byte[] plaintext, SecretKey key, byte[] iv) throws Exception { Cipher cipher = Cipher.getInstance(AES_CIPHER); IvParameterSpec ivSpec = new IvParameterSpec(iv); @@ -51,4 +65,30 @@ public class EncryptionUtil { cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); return cipher.doFinal(ciphertext); } + + // ------------------------ RSA ------------------------ + + public static byte[] encryptRSA(byte[] data, PublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return cipher.doFinal(data); + } + + public static byte[] decryptRSA(byte[] data, PrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_CIPHER); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(data); + } + + // --------- Encrypt/decrypt RSA private key using AES derived from password --------- + + public static byte[] encryptPrivateKey(PrivateKey privateKey, String password, byte[] salt, byte[] iv) throws Exception { + SecretKey aesKey = deriveKey(password.toCharArray(), salt); + return encrypt(privateKey.getEncoded(), aesKey, iv); + } + + public static byte[] decryptPrivateKey(byte[] encryptedPrivateKey, String password, byte[] salt, byte[] iv) throws Exception { + SecretKey aesKey = deriveKey(password.toCharArray(), salt); + return decrypt(encryptedPrivateKey, aesKey, iv); + } } \ No newline at end of file diff --git a/src/main/java/com/skycrate/backend/skycrateBackend/utils/RSAKeyUtil.java b/src/main/java/com/skycrate/backend/skycrateBackend/utils/RSAKeyUtil.java index fc97d9b..bde1801 100644 --- a/src/main/java/com/skycrate/backend/skycrateBackend/utils/RSAKeyUtil.java +++ b/src/main/java/com/skycrate/backend/skycrateBackend/utils/RSAKeyUtil.java @@ -29,44 +29,38 @@ public class RSAKeyUtil { return keyFactory.generatePrivate(spec); } + // Shorthand decode aliases + public static PublicKey decodePublicKey(byte[] publicKeyBytes) throws Exception { + return getPublicKeyFromBytes(publicKeyBytes); + } + + public static PrivateKey decodePrivateKey(byte[] privateKeyBytes) throws Exception { + return getPrivateKeyFromBytes(privateKeyBytes); + } + // Encrypt data using RSA (with padding) public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); // Specify padding + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return cipher.doFinal(data); } // Decrypt data using RSA (with padding) public static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception { - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); // Specify padding + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(encryptedData); } - // Generate AES Key (128, 192, or 256 bits) + // AES key generation public static SecretKey generateAESKey(int keySize) throws NoSuchAlgorithmException { if (keySize != 128 && keySize != 192 && keySize != 256) { throw new IllegalArgumentException("Invalid AES key size. Must be 128, 192, or 256 bits."); } KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - keyGenerator.init(keySize); // Specify the key size + keyGenerator.init(keySize); return keyGenerator.generateKey(); } -// -// // Encrypt AES Key using RSA -// public static byte[] encryptAESKey(SecretKey aesKey, PublicKey publicKey) throws Exception { -// return encrypt(aesKey.getEncoded(), publicKey); // Encrypt the AES key using RSA -// } -// -// // Decrypt AES Key using RSA -// public static SecretKey decryptAESKey(byte[] encryptedAESKey, PrivateKey privateKey, int keySize) throws Exception { -// byte[] decryptedKey = decrypt(encryptedAESKey, privateKey); // Decrypt with RSA -// // Ensure that the decrypted key length matches the expected AES key size -// if (decryptedKey.length != keySize / 8) { -// throw new IllegalArgumentException("Decrypted key size does not match expected AES key size."); -// } -// return new SecretKeySpec(decryptedKey, 0, decryptedKey.length, "AES"); // Convert to AES Key -// } public static byte[] encryptAESKey(SecretKey aesKey, PublicKey publicKey) throws Exception { return encrypt(aesKey.getEncoded(), publicKey); @@ -76,5 +70,4 @@ public class RSAKeyUtil { byte[] decryptedKey = decrypt(encryptedAESKey, privateKey); return new SecretKeySpec(decryptedKey, 0, decryptedKey.length, "AES"); } - -} +} \ No newline at end of file