import { createIV, getImportedKey } from "./index"; import { ExcalidrawElement } from "../element/types"; import { getSceneVersion } from "../element"; import Portal from "../components/Portal"; import { restoreElements } from "./restore"; let firebasePromise: Promise< typeof import("firebase/app").default > | null = null; const loadFirebase = async () => { const firebase = ( await import(/* webpackChunkName: "firebase" */ "firebase/app") ).default; await import(/* webpackChunkName: "firestore" */ "firebase/firestore"); const firebaseConfig = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG); firebase.initializeApp(firebaseConfig); return firebase; }; const getFirebase = async (): Promise< typeof import("firebase/app").default > => { if (!firebasePromise) { firebasePromise = loadFirebase(); } const firebase = await firebasePromise!; return firebase; }; interface FirebaseStoredScene { sceneVersion: number; iv: firebase.default.firestore.Blob; ciphertext: firebase.default.firestore.Blob; } const encryptElements = async ( key: string, elements: readonly ExcalidrawElement[], ): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> => { const importedKey = await getImportedKey(key, "encrypt"); const iv = createIV(); const json = JSON.stringify(elements); const encoded = new TextEncoder().encode(json); const ciphertext = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv, }, importedKey, encoded, ); return { ciphertext, iv }; }; const decryptElements = async ( key: string, iv: Uint8Array, ciphertext: ArrayBuffer, ): Promise => { const importedKey = await getImportedKey(key, "decrypt"); const decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv, }, importedKey, ciphertext, ); const decodedData = new TextDecoder("utf-8").decode( new Uint8Array(decrypted) as any, ); return JSON.parse(decodedData); }; const firebaseSceneVersionCache = new WeakMap(); export const isSavedToFirebase = ( portal: Portal, elements: readonly ExcalidrawElement[], ): boolean => { if (portal.socket && portal.roomID && portal.roomKey) { const sceneVersion = getSceneVersion(elements); return firebaseSceneVersionCache.get(portal.socket) === sceneVersion; } // if no room exists, consider the room saved so that we don't unnecessarily // prevent unload (there's nothing we could do at that point anyway) return true; }; export const saveToFirebase = async ( portal: Portal, elements: readonly ExcalidrawElement[], ) => { const { roomID, roomKey, socket } = portal; if ( // if no room exists, consider the room saved because there's nothing we can // do at this point !roomID || !roomKey || !socket || isSavedToFirebase(portal, elements) ) { return true; } const firebase = await getFirebase(); const sceneVersion = getSceneVersion(elements); const { ciphertext, iv } = await encryptElements(roomKey, elements); const nextDocData = { sceneVersion, ciphertext: firebase.firestore.Blob.fromUint8Array( new Uint8Array(ciphertext), ), iv: firebase.firestore.Blob.fromUint8Array(iv), } as FirebaseStoredScene; const db = firebase.firestore(); const docRef = db.collection("scenes").doc(roomID); const didUpdate = await db.runTransaction(async (transaction) => { const doc = await transaction.get(docRef); if (!doc.exists) { transaction.set(docRef, nextDocData); return true; } const prevDocData = doc.data() as FirebaseStoredScene; if (prevDocData.sceneVersion >= nextDocData.sceneVersion) { return false; } transaction.update(docRef, nextDocData); return true; }); if (didUpdate) { firebaseSceneVersionCache.set(socket, sceneVersion); } return didUpdate; }; export const loadFromFirebase = async ( roomID: string, roomKey: string, ): Promise => { const firebase = await getFirebase(); const db = firebase.firestore(); const docRef = db.collection("scenes").doc(roomID); const doc = await docRef.get(); if (!doc.exists) { return null; } const storedScene = doc.data() as FirebaseStoredScene; const ciphertext = storedScene.ciphertext.toUint8Array(); const iv = storedScene.iv.toUint8Array(); return restoreElements(await decryptElements(roomKey, iv, ciphertext)); };