Pārlūkot izejas kodu

fix: use random IV for link-sharing encryption (#2829) (#2833)

* fix: use random IV for link-sharing encryption (#2829)

* fix: add backward compatibility for link-sharing encryption (#2829)
Mike Kowalski 4 gadi atpakaļ
vecāks
revīzija
c8743a8c02
1 mainītis faili ar 41 papildinājumiem un 15 dzēšanām
  1. 41 15
      src/excalidraw-app/data/index.ts

+ 41 - 15
src/excalidraw-app/data/index.ts

@@ -80,8 +80,10 @@ export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSour
   _brand: "socketUpdateData";
 };
 
+const IV_LENGTH_BYTES = 12; // 96 bits
+
 export const createIV = () => {
-  const arr = new Uint8Array(12);
+  const arr = new Uint8Array(IV_LENGTH_BYTES);
   return window.crypto.getRandomValues(arr);
 };
 
@@ -175,6 +177,22 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
     [usage],
   );
 
+const decryptImported = async (
+  iv: ArrayBuffer,
+  encrypted: ArrayBuffer,
+  privateKey: string,
+): Promise<ArrayBuffer> => {
+  const key = await getImportedKey(privateKey, "decrypt");
+  return window.crypto.subtle.decrypt(
+    {
+      name: "AES-GCM",
+      iv,
+    },
+    key,
+    encrypted,
+  );
+};
+
 const importFromBackend = async (
   id: string | null,
   privateKey?: string | null,
@@ -183,6 +201,7 @@ const importFromBackend = async (
     const response = await fetch(
       privateKey ? `${BACKEND_V2_GET}${id}` : `${BACKEND_GET}${id}.json`,
     );
+
     if (!response.ok) {
       window.alert(t("alerts.importBackendFailed"));
       return {};
@@ -190,16 +209,19 @@ const importFromBackend = async (
     let data: ImportedDataState;
     if (privateKey) {
       const buffer = await response.arrayBuffer();
-      const key = await getImportedKey(privateKey, "decrypt");
-      const iv = new Uint8Array(12);
-      const decrypted = await window.crypto.subtle.decrypt(
-        {
-          name: "AES-GCM",
-          iv,
-        },
-        key,
-        buffer,
-      );
+
+      let decrypted: ArrayBuffer;
+      try {
+        // Buffer should contain both the IV (fixed length) and encrypted data
+        const iv = buffer.slice(0, IV_LENGTH_BYTES);
+        const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
+        decrypted = await decryptImported(iv, encrypted, privateKey);
+      } catch (error) {
+        // Fixed IV (old format, backward compatibility)
+        const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
+        decrypted = await decryptImported(fixedIv, buffer, privateKey);
+      }
+
       // We need to convert the decrypted array buffer to a string
       const string = new window.TextDecoder("utf-8").decode(
         new Uint8Array(decrypted) as any,
@@ -263,9 +285,8 @@ export const exportToBackend = async (
     true, // extractable
     ["encrypt", "decrypt"],
   );
-  // The iv is set to 0. We are never going to reuse the same key so we don't
-  // need to have an iv. (I hope that's correct...)
-  const iv = new Uint8Array(12);
+
+  const iv = createIV();
   // We use symmetric encryption. AES-GCM is the recommended algorithm and
   // includes checks that the ciphertext has not been modified by an attacker.
   const encrypted = await window.crypto.subtle.encrypt(
@@ -276,6 +297,11 @@ export const exportToBackend = async (
     key,
     encoded,
   );
+
+  // Concatenate IV with encrypted data (IV does not have to be secret).
+  const payloadBlob = new Blob([iv.buffer, encrypted]);
+  const payload = await new Response(payloadBlob).arrayBuffer();
+
   // We use jwk encoding to be able to extract just the base64 encoded key.
   // We will hardcode the rest of the attributes when importing back the key.
   const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
@@ -283,7 +309,7 @@ export const exportToBackend = async (
   try {
     const response = await fetch(BACKEND_V2_POST, {
       method: "POST",
-      body: encrypted,
+      body: payload,
     });
     const json = await response.json();
     if (json.id) {