Add SHA-256 checksums to the uploaded data
authorAlexander Ebert <ebert@woltlab.com>
Wed, 27 Dec 2023 16:54:10 +0000 (17:54 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 27 Mar 2024 22:55:26 +0000 (23:55 +0100)
ts/WoltLabSuite/Core/Component/File/Upload.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js
wcfsetup/install/files/lib/action/FileUploadAction.class.php
wcfsetup/install/files/lib/action/FileUploadPreflightAction.class.php
wcfsetup/setup/db/install.sql

index 5e0236741a98ab37b22584d583917e36c42690d3..85e6925845edb128e0e3a38ee05fb018e5655c74 100644 (file)
@@ -6,10 +6,13 @@ type PreflightResponse = {
 };
 
 async function upload(element: WoltlabCoreFileUploadElement, file: File): Promise<void> {
+  const fileHash = await getSha256Hash(await file.arrayBuffer());
+
   const response = (await prepareRequest(element.dataset.endpoint!)
     .post({
       filename: file.name,
-      filesize: file.size,
+      fileSize: file.size,
+      fileHash,
     })
     .fetchAsJson()) as PreflightResponse;
   const { endpoints } = response;
@@ -17,30 +20,26 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis
   const chunkSize = 2_000_000;
   const chunks = Math.ceil(file.size / chunkSize);
 
-  const arrayBufferToHex = (buffer: ArrayBuffer): string => {
-    return Array.from(new Uint8Array(buffer))
-      .map((b) => b.toString(16).padStart(2, "0"))
-      .join("");
-  };
-
-  const hash = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer());
-  console.log("checksum for the entire file is:", arrayBufferToHex(hash));
-
-  const data: Blob[] = [];
   for (let i = 0; i < chunks; i++) {
     const start = i * chunkSize;
     const end = start + chunkSize;
     const chunk = file.slice(start, end);
-    data.push(chunk);
 
-    console.log("Uploading", start, "to", end, " (total: " + chunk.size + " of " + file.size + ")");
+    const endpoint = new URL(endpoints[i]);
 
-    await prepareRequest(endpoints[i]).post(chunk).fetchAsResponse();
+    const checksum = await getSha256Hash(await chunk.arrayBuffer());
+    endpoint.searchParams.append("checksum", checksum);
+
+    await prepareRequest(endpoint.toString()).post(chunk).fetchAsResponse();
   }
+}
+
+async function getSha256Hash(data: BufferSource): Promise<string> {
+  const buffer = await window.crypto.subtle.digest("SHA-256", data);
 
-  const uploadedChunks = new Blob(data);
-  const uploadedHash = await window.crypto.subtle.digest("SHA-256", await uploadedChunks.arrayBuffer());
-  console.log("checksum for the entire file is:", arrayBufferToHex(uploadedHash));
+  return Array.from(new Uint8Array(buffer))
+    .map((b) => b.toString(16).padStart(2, "0"))
+    .join("");
 }
 
 export function setup(): void {
index 2c6a0e6dd4021e4effda5c7c725a120a9d138417..48adb30f3bca6754269b59d8e2ded8362e68293e 100644 (file)
@@ -3,34 +3,32 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Co
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.setup = void 0;
     async function upload(element, file) {
+        const fileHash = await getSha256Hash(await file.arrayBuffer());
         const response = (await (0, Backend_1.prepareRequest)(element.dataset.endpoint)
             .post({
             filename: file.name,
-            filesize: file.size,
+            fileSize: file.size,
+            fileHash,
         })
             .fetchAsJson());
         const { endpoints } = response;
         const chunkSize = 2000000;
         const chunks = Math.ceil(file.size / chunkSize);
-        const arrayBufferToHex = (buffer) => {
-            return Array.from(new Uint8Array(buffer))
-                .map((b) => b.toString(16).padStart(2, "0"))
-                .join("");
-        };
-        const hash = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer());
-        console.log("checksum for the entire file is:", arrayBufferToHex(hash));
-        const data = [];
         for (let i = 0; i < chunks; i++) {
             const start = i * chunkSize;
             const end = start + chunkSize;
             const chunk = file.slice(start, end);
-            data.push(chunk);
-            console.log("Uploading", start, "to", end, " (total: " + chunk.size + " of " + file.size + ")");
-            await (0, Backend_1.prepareRequest)(endpoints[i]).post(chunk).fetchAsResponse();
+            const endpoint = new URL(endpoints[i]);
+            const checksum = await getSha256Hash(await chunk.arrayBuffer());
+            endpoint.searchParams.append("checksum", checksum);
+            await (0, Backend_1.prepareRequest)(endpoint.toString()).post(chunk).fetchAsResponse();
         }
-        const uploadedChunks = new Blob(data);
-        const uploadedHash = await window.crypto.subtle.digest("SHA-256", await uploadedChunks.arrayBuffer());
-        console.log("checksum for the entire file is:", arrayBufferToHex(uploadedHash));
+    }
+    async function getSha256Hash(data) {
+        const buffer = await window.crypto.subtle.digest("SHA-256", data);
+        return Array.from(new Uint8Array(buffer))
+            .map((b) => b.toString(16).padStart(2, "0"))
+            .join("");
     }
     function setup() {
         (0, Selector_1.wheneverFirstSeen)("woltlab-core-file-upload", (element) => {
index 4e14983b4e34b8bac99e71574edbf2d5fd83fd72..1fcbb6c7a9ce447d69847542c0fff4d1552d9cb1 100644 (file)
@@ -9,6 +9,7 @@ use Psr\Http\Server\RequestHandlerInterface;
 use wcf\http\Helper;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\io\AtomicWriter;
+use wcf\system\io\File;
 use wcf\system\WCF;
 
 final class FileUploadAction implements RequestHandlerInterface
@@ -20,6 +21,7 @@ final class FileUploadAction implements RequestHandlerInterface
             $request->getQueryParams(),
             <<<'EOT'
                     array {
+                        checksum: non-empty-string,
                         identifier: non-empty-string,
                         sequenceNo: int,
                     }
@@ -34,14 +36,31 @@ final class FileUploadAction implements RequestHandlerInterface
         $row = $statement->fetchSingleRow();
 
         if ($row === false) {
+            // TODO: Proper error message
             throw new IllegalLinkException();
         }
 
         // Check if this is a valid sequence no.
         // TODO: The chunk calculation shouldn’t be based on a fixed number.
         $chunkSize = 2_000_000;
-        $chunks = (int)\ceil($row['filesize'] / $chunkSize);
+        $chunks = (int)\ceil($row['fileSize'] / $chunkSize);
         if ($parameters['sequenceNo'] >= $chunks) {
+            // TODO: Proper error message
+            throw new IllegalLinkException();
+        }
+
+        // Check if the checksum matches the received data.
+        $ctx = \hash_init('sha256');
+        $bufferSize = 1 * 1024 * 1024;
+        $stream = $request->getBody();
+        while (!$stream->eof()) {
+            \hash_update($ctx, $stream->read($bufferSize));
+        }
+        $result = \hash_final($ctx);
+        $stream->rewind();
+
+        if ($result !== $parameters['checksum']) {
+            // TODO: Proper error message
             throw new IllegalLinkException();
         }
 
@@ -65,16 +84,14 @@ final class FileUploadAction implements RequestHandlerInterface
 
         // Write the chunk using a buffer to avoid blowing up the memory limit.
         // See https://stackoverflow.com/a/61997147
-        $file = new AtomicWriter($tmpPath . $filename);
+        $result = new AtomicWriter($tmpPath . $filename);
         $bufferSize = 1 * 1024 * 1024;
 
-        $fh = \fopen('php://input', 'rb');
-        while (!\feof($fh)) {
-            $file->write(\fread($fh, $bufferSize));
+        while (!$stream->eof()) {
+            $result->write($stream->read($bufferSize));
         }
-        \fclose($fh);
 
-        $file->flush();
+        $result->flush();
 
         // Check if we have all chunks.
         $data = [];
@@ -97,16 +114,16 @@ final class FileUploadAction implements RequestHandlerInterface
             $bufferSize = 1 * 1024 * 1024;
 
             $newFilename = \sprintf('%s-final.bin', $row['identifier']);
-            $file = new AtomicWriter($tmpPath . $newFilename);
+            $result = new AtomicWriter($tmpPath . $newFilename);
             foreach ($data as $fileChunk) {
-                $fh = \fopen($fileChunk, 'rb');
-                while (!\feof($fh)) {
-                    $file->write(\fread($fh, $bufferSize));
+                $source = new File($fileChunk, 'rb');
+                while (!$source->eof()) {
+                    $result->write($source->read($bufferSize));
                 }
-                \fclose($fh);
+                $source->close();
             }
 
-            $file->flush();
+            $result->flush();
 
             \wcfDebug(
                 \memory_get_peak_usage(true),
index 1fd23b200f84fb8d0662e765f11966a77afaee5a..42ae6e952b8a2baf73a7114942e6f55e1d0987b0 100644 (file)
@@ -20,14 +20,15 @@ final class FileUploadPreflightAction implements RequestHandlerInterface
             <<<'EOT'
                     array {
                         filename: non-empty-string,
-                        filesize: positive-int,
+                        fileSize: positive-int,
+                        fileHash: non-empty-string,
                     }
                     EOT,
         );
 
         // TODO: The chunk calculation shouldn’t be based on a fixed number.
         $chunkSize = 2_000_000;
-        $chunks = (int)\ceil($parameters['filesize'] / $chunkSize);
+        $chunks = (int)\ceil($parameters['fileSize'] / $chunkSize);
 
         $identifier = $this->createTemporaryFile($parameters);
 
@@ -52,14 +53,15 @@ final class FileUploadPreflightAction implements RequestHandlerInterface
         $identifier = \bin2hex(\random_bytes(20));
 
         $sql = "INSERT INTO     wcf1_file_temporary
-                                (identifier, time, filename, filesize)
-                         VALUES (?, ?, ?, ?)";
+                                (identifier, time, filename, fileSize, fileHash)
+                         VALUES (?, ?, ?, ?, ?)";
         $statement = WCF::getDB()->prepare($sql);
         $statement->execute([
             $identifier,
             \TIME_NOW,
             $parameters['filename'],
-            $parameters['filesize'],
+            $parameters['fileSize'],
+            $parameters['fileHash'],
         ]);
 
         return $identifier;
index cb59bd13b4c5e1d0974559f7cbeb22ff29c57a00..c3520a4d55733f1ab73d32406f0f10209c73f38b 100644 (file)
@@ -598,7 +598,8 @@ CREATE TABLE wcf1_file_temporary (
        identifier CHAR(40) NOT NULL PRIMARY KEY,
        time INT NOT NULL,
        filename VARCHAR(255) NOT NULL,
-       filesize BIGINT NOT NULL
+       fileSize BIGINT NOT NULL,
+       fileHash CHAR(64) NOT NULL
 );
 
 DROP TABLE IF EXISTS wcf1_file_chunk;