I’m a newbie in this topic, first time when I want to do something like this.
I’m implementing file encryption and decryption using AES/GCM/NoPadding in a Spring Boot application. The goal is to encrypt a file during upload and decrypt it during download. However, I’m encountering an issue where the decrypted file is larger than the original and results in corrupted files (e.g., blank pages in PDFs and JPEG decode errors).
My AESUtil
class looks like this, where I have the encryption/decryption methods:
private static final String AES_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_SIZE = 12;
private static final int TAG_BIT_LENGTH = 128;
public static void encryptFile(SecretKey key, InputStream inputFile, Path outputFile) throws Exception {
Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
byte[] iv = new byte[IV_SIZE];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
try (OutputStream outputStream = Files.newOutputStream(outputFile)) {
outputStream.write(iv);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputFile.read(buffer)) != -1) {
byte[] output = cipher.update(buffer, 0, bytesRead);
if (output != null) {
outputStream.write(output);
}
}
byte[] outputBytes = cipher.doFinal();
if (outputBytes != null) {
outputStream.write(outputBytes);
}
}
}
public static void decryptFile(SecretKey key, Path encryptedFile, Path decryptedFile) throws Exception {
try (InputStream inputStream = Files.newInputStream(encryptedFile);
OutputStream outputStream = Files.newOutputStream(decryptedFile)) {
byte[] iv = new byte[IV_SIZE];
if (inputStream.read(iv) != IV_SIZE) {
throw new IllegalArgumentException("Invalid AES IV size.");
}
Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byte[] output = cipher.update(buffer, 0, bytesRead);
if (output != null) {
outputStream.write(output);
}
}
byte[] outputBytes = cipher.doFinal();
if (outputBytes != null) {
outputStream.write(outputBytes);
}
}
}
My Service
methods for uploading and downloading the files:
@Override
public Long uploadFile(String loggedInUserId, MultipartFile file) {
String hashedFolderName = HashUtil.hashString(loggedInUserId);
Path userFolderPath = Paths.get(baseUploadDirectory, hashedFolderName);
fileExtensionValidator.validate(file);
createDirectory(userFolderPath);
String originalFileName = file.getOriginalFilename();
Long currentMilliSec = generateCurrentMilliSec(ZonedDateTime.now());
String newFileName = currentMilliSec + "-" + originalFileName;
Path filePath = userFolderPath.resolve(newFileName);
FileUploadEntity uploadedFile = getFileUploadEntity(loggedInUserId, originalFileName, newFileName, filePath);
try (InputStream inputStream = file.getInputStream()) {
AESFileUtil.encryptFile(aesKeyProvider.getSecretKey(), inputStream, filePath);
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt and store file " + file.getOriginalFilename(), e);
}
return uploadedFile.getId();
}
@Override
public Resource load(Long id) {
FileDto file = fileUploadRepository.findById(id)
.map(fileUploadMapper::toFileDto)
.orElseThrow(() -> new FileNotFoundException("File not found with id " + id));
try {
Path filePath = Path.of(file.getLocation());
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists() && resource.isReadable()) {
return resource;
} else {
throw new FileNotFoundException("File not found or not readable at location: " + file.getLocation());
}
} catch (Exception ex) {
throw new FileNotFoundException("File not found or not readable at location: " + file.getLocation());
}
}
private void createDirectory(Path userFolderPath) {
try {
if (!Files.exists(userFolderPath)) {
Files.createDirectories(userFolderPath);
}
} catch (IOException e) {
throw new RuntimeException("Failed to create user directory", e);
}
}
And my Controller
with the two endpoints:
@PostMapping(value = "/upload", consumes = MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Long> uploadFile(@RequestParam MultipartFile file) {
String loggedInUserId = facade.getLoggedInUUID();
return ResponseEntity.ok().body(fileUploadService.uploadFile(loggedInUserId, file));
}
@GetMapping("/download/{id}")
public ResponseEntity<Resource> downloadFile(@PathVariable Long id) {
Resource fileResource = fileUploadService.load(id);
try {
SecretKey key = aesKeyProvider.getSecretKey();
Path filePath = Path.of(fileResource.getFile().getAbsolutePath());
Path tempDecryptedFile = Files.createTempFile("decrypted", ".tmp");
logFileSize(filePath, "Encrypted");
AESFileUtil.decryptFile(key, filePath, tempDecryptedFile);
logFileSize(tempDecryptedFile, "Decrypted");
Resource decryptedResource = new FileSystemResource(tempDecryptedFile.toFile());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="" + fileResource.getFilename() + """)
.body(decryptedResource);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt file", e);
}
}
private void logFileSize(Path path, String description) throws IOException {
long size = Files.size(path);
System.out.println(description + " file size: " + size + " bytes");
}
For a 1.3 MB PDF the encryption succeeds and I can not open it from its folder.
But, If I download it, the encrypted file downloads correctly, and when I want to open it I get a decode error, and in the console I can see that the decrypted file has a different size.
Encrypted file size: 1341219 bytes
Decrypted file size: 1341191 bytes
I ensured IV is correctly handled by writing it at the start of the file during encryption and reading it during decryption. And I also used Cipher.update()
and Cipher.doFinal()
correctly to process file data in chunks.
What could be causing the file size discrepancy and data corruption during AES/GCM/NoPadding encryption and decryption? How can I ensure the decrypted file is identical to the original?
Is there a problem in the Controller
?