From 0c3f8353be2a1e4ebf31e25cb008245333f56dec Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 5 Jun 2026 14:29:45 +0200 Subject: [PATCH] get rid of backwards compatibility complexity --- .../controller/DownloadController.java | 53 +--------------- .../gregor_lohaus/gtransfer/model/File.java | 3 - .../filecleanup/FileCleanupService.java | 5 -- Backend/src/main/resources/static/crypto.js | 62 +++++++------------ Backend/src/main/resources/static/upload.js | 19 +++--- 5 files changed, 31 insertions(+), 111 deletions(-) diff --git a/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java b/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java index d6b6864..38bfef6 100644 --- a/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java +++ b/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -65,7 +64,7 @@ public class DownloadController { return ResponseEntity.badRequest().build(); } - Optional data = storageService.get(storageKey(file, index)); + Optional data = storageService.get(StorageKeys.chunk(file.getId(), index)); if (data.isEmpty()) { return ResponseEntity.notFound().build(); } @@ -96,39 +95,6 @@ public class DownloadController { return ResponseEntity.noContent().build(); } - @GetMapping("/download/{id}/data") - @ResponseBody - @Transactional - public ResponseEntity data(@PathVariable String id) { - AvailableFile available = getAvailableFile(id); - if (!available.found()) { - return ResponseEntity.status(available.status()).build(); - } - - File file = available.file(); - if (file.isChunked()) { - return ResponseEntity.status(HttpStatus.GONE).build(); - } - - Optional data = storageService.get(id); - if (data.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - file.setDownloads(file.getDownloads() + 1); - fileRepository.save(file); - - if (file.getDownloadLimit() != null && file.getDownloads() >= file.getDownloadLimit()) { - deleteStoredFile(file); - fileRepository.delete(file); - } - - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition(file.getName())) - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(data.get()); - } - private AvailableFile getAvailableFile(String id) { Optional fileOpt = fileRepository.findById(id); if (fileOpt.isEmpty()) { @@ -152,28 +118,11 @@ public class DownloadController { } private void deleteStoredFile(File file) { - if (!file.isChunked()) { - storageService.delete(file.getId()); - return; - } - for (int i = 0; i < file.getChunkCount(); i++) { storageService.delete(StorageKeys.chunk(file.getId(), i)); } } - private String storageKey(File file, int index) { - if (!file.isChunked()) { - return file.getId(); - } - return StorageKeys.chunk(file.getId(), index); - } - - private String contentDisposition(String filename) { - return "attachment; filename=\"" - + filename.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; - } - private record AvailableFile(File file, HttpStatus status) { static AvailableFile ok(File file) { return new AvailableFile(file, HttpStatus.OK); diff --git a/Backend/src/main/java/com/gregor_lohaus/gtransfer/model/File.java b/Backend/src/main/java/com/gregor_lohaus/gtransfer/model/File.java index faa9e39..441b6d3 100644 --- a/Backend/src/main/java/com/gregor_lohaus/gtransfer/model/File.java +++ b/Backend/src/main/java/com/gregor_lohaus/gtransfer/model/File.java @@ -49,9 +49,6 @@ public class File { public void setChunkCount(Integer chunkCount) { this.chunkCount = chunkCount; } - public boolean isChunked() { - return chunkCount != null; - } public Long getSize() { return size; } diff --git a/Backend/src/main/java/com/gregor_lohaus/gtransfer/services/filecleanup/FileCleanupService.java b/Backend/src/main/java/com/gregor_lohaus/gtransfer/services/filecleanup/FileCleanupService.java index 502759a..41d0780 100644 --- a/Backend/src/main/java/com/gregor_lohaus/gtransfer/services/filecleanup/FileCleanupService.java +++ b/Backend/src/main/java/com/gregor_lohaus/gtransfer/services/filecleanup/FileCleanupService.java @@ -51,11 +51,6 @@ public class FileCleanupService { } private void deleteStoredFile(File file) { - if (!file.isChunked()) { - storageService.delete(file.getId()); - return; - } - for (int i = 0; i < file.getChunkCount(); i++) { storageService.delete(StorageKeys.chunk(file.getId(), i)); } diff --git a/Backend/src/main/resources/static/crypto.js b/Backend/src/main/resources/static/crypto.js index 89cb261..7fa15ac 100644 --- a/Backend/src/main/resources/static/crypto.js +++ b/Backend/src/main/resources/static/crypto.js @@ -1,6 +1,6 @@ var DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024; -async function generateFileKey() { +async function encryptFile(file, chunkSize = DEFAULT_CHUNK_SIZE) { const key = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, @@ -10,47 +10,31 @@ async function generateFileKey() { const rawKey = await crypto.subtle.exportKey('raw', key); const hash = await hashKey(rawKey); const base64urlKey = encodeKey(rawKey); + const chunkCount = Math.max(1, Math.ceil(file.size / chunkSize)); - return { key, rawKey, hash, base64urlKey }; + return { + hash, + base64urlKey, + chunkCount, + size: file.size, + chunks: encryptedChunks(file, key, chunkCount, chunkSize) + }; } -async function encryptFileChunk(file, index, key, chunkSize = DEFAULT_CHUNK_SIZE) { - const start = index * chunkSize; - const end = Math.min(start + chunkSize, file.size); - const plaintext = await file.slice(start, end).arrayBuffer(); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext); +async function* encryptedChunks(file, key, chunkCount, chunkSize) { + for (let index = 0; index < chunkCount; index++) { + const start = index * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const plaintext = await file.slice(start, end).arrayBuffer(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext); - const payload = new Uint8Array(12 + ciphertext.byteLength); - payload.set(iv, 0); - payload.set(new Uint8Array(ciphertext), 12); + const payload = new Uint8Array(12 + ciphertext.byteLength); + payload.set(iv, 0); + payload.set(new Uint8Array(ciphertext), 12); - return payload; -} - -async function encryptFile(arrayBuffer) { - const key = await crypto.subtle.generateKey( - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - - const iv = crypto.getRandomValues(new Uint8Array(12)); - - const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, arrayBuffer); - - // Payload: 12-byte IV prepended to ciphertext - const payload = new Uint8Array(12 + ciphertext.byteLength); - payload.set(iv, 0); - payload.set(new Uint8Array(ciphertext), 12); - - // SHA-256(rawKey) → file identifier sent to server; server never sees the key itself - const rawKey = await crypto.subtle.exportKey('raw', key); - const hash = await hashKey(rawKey); - - const base64urlKey = encodeKey(rawKey); - - return { payload, hash, base64urlKey }; + yield { index, payload }; + } } async function hashKey(rawKey) { @@ -59,10 +43,6 @@ async function hashKey(rawKey) { .join(''); } -async function decryptFile(payload, key) { - return decryptChunk(payload, key); -} - async function decryptChunk(payload, key) { const bytes = new Uint8Array(payload); const iv = bytes.slice(0, 12); diff --git a/Backend/src/main/resources/static/upload.js b/Backend/src/main/resources/static/upload.js index 3dd2a55..26a6ded 100644 --- a/Backend/src/main/resources/static/upload.js +++ b/Backend/src/main/resources/static/upload.js @@ -50,17 +50,16 @@ async function startUpload() { { swapStyle: 'innerHTML' }); try { - const { key, hash, base64urlKey } = await generateFileKey(); - const chunkCount = Math.max(1, Math.ceil(selectedFile.size / DEFAULT_CHUNK_SIZE)); + const encryptedFile = await encryptFile(selectedFile); - for (let index = 0; index < chunkCount; index++) { + for await (const { index, payload } of encryptedFile.chunks) { + const chunkCount = encryptedFile.chunkCount; setProgress(`Encrypting chunk ${index + 1} of ${chunkCount}\u2026`, index, chunkCount); - const payload = await encryptFileChunk(selectedFile, index, key); setProgress(`Uploading chunk ${index + 1} of ${chunkCount}\u2026`, index + 0.5, chunkCount); const chunkData = new FormData(); chunkData.append('chunk', new Blob([payload]), String(index)); - chunkData.append('hash', hash); + chunkData.append('hash', encryptedFile.hash); chunkData.append('index', index); const chunkResponse = await fetch('/upload/chunk', { method: 'POST', body: chunkData }); @@ -68,12 +67,12 @@ async function startUpload() { setProgress(`Uploaded chunk ${index + 1} of ${chunkCount}`, index + 1, chunkCount); } - setProgress('Finalizing\u2026', chunkCount, chunkCount); + setProgress('Finalizing\u2026', encryptedFile.chunkCount, encryptedFile.chunkCount); const metadata = new FormData(); - metadata.append('hash', hash); + metadata.append('hash', encryptedFile.hash); metadata.append('name', selectedFile.name); - metadata.append('chunkCount', chunkCount); - metadata.append('size', selectedFile.size); + metadata.append('chunkCount', encryptedFile.chunkCount); + metadata.append('size', encryptedFile.size); if (expiryDays) metadata.append('expiryDays', expiryDays); if (downloadLimit) metadata.append('downloadLimit', downloadLimit); @@ -82,7 +81,7 @@ async function startUpload() { htmx.swap(dropZone, await response.text(), { swapStyle: 'innerHTML' }); htmx.process(dropZone); - htmx.find('#share-link').value = window.location.origin + '/download#' + base64urlKey; + htmx.find('#share-link').value = window.location.origin + '/download#' + encryptedFile.base64urlKey; } catch (err) { htmx.swap(dropZone, `