2020-12-05 20:00:53 +05:30
|
|
|
import { getImportedKey } from "../data";
|
|
|
|
import { createIV } from "./index";
|
|
|
|
import { ExcalidrawElement } from "../../element/types";
|
|
|
|
import { getSceneVersion } from "../../element";
|
|
|
|
import Portal from "../collab/Portal";
|
|
|
|
import { restoreElements } from "../../data/restore";
|
2020-10-04 11:12:47 -07:00
|
|
|
|
2020-10-29 16:10:46 +01:00
|
|
|
let firebasePromise: Promise<
|
|
|
|
typeof import("firebase/app").default
|
|
|
|
> | null = null;
|
2020-10-04 11:12:47 -07:00
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
const loadFirebase = async () => {
|
2020-10-29 16:10:46 +01:00
|
|
|
const firebase = (
|
|
|
|
await import(/* webpackChunkName: "firebase" */ "firebase/app")
|
|
|
|
).default;
|
2020-10-18 23:06:25 +05:30
|
|
|
await import(/* webpackChunkName: "firestore" */ "firebase/firestore");
|
2020-10-04 11:12:47 -07:00
|
|
|
|
|
|
|
const firebaseConfig = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
|
|
|
|
firebase.initializeApp(firebaseConfig);
|
|
|
|
|
|
|
|
return firebase;
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-10-04 11:12:47 -07:00
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
const getFirebase = async (): Promise<
|
|
|
|
typeof import("firebase/app").default
|
|
|
|
> => {
|
2020-10-04 11:12:47 -07:00
|
|
|
if (!firebasePromise) {
|
|
|
|
firebasePromise = loadFirebase();
|
|
|
|
}
|
2020-12-05 20:00:53 +05:30
|
|
|
return await firebasePromise!;
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-10-04 11:12:47 -07:00
|
|
|
|
|
|
|
interface FirebaseStoredScene {
|
|
|
|
sceneVersion: number;
|
2020-10-29 16:10:46 +01:00
|
|
|
iv: firebase.default.firestore.Blob;
|
|
|
|
ciphertext: firebase.default.firestore.Blob;
|
2020-10-04 11:12:47 -07:00
|
|
|
}
|
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
const encryptElements = async (
|
2020-10-04 11:12:47 -07:00
|
|
|
key: string,
|
|
|
|
elements: readonly ExcalidrawElement[],
|
2020-11-06 22:06:39 +02:00
|
|
|
): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> => {
|
2020-10-04 11:12:47 -07:00
|
|
|
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 };
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-10-04 11:12:47 -07:00
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
const decryptElements = async (
|
2020-10-04 11:12:47 -07:00
|
|
|
key: string,
|
|
|
|
iv: Uint8Array,
|
|
|
|
ciphertext: ArrayBuffer,
|
2020-11-06 22:06:39 +02:00
|
|
|
): Promise<readonly ExcalidrawElement[]> => {
|
2020-10-04 11:12:47 -07:00
|
|
|
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);
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-10-04 11:12:47 -07:00
|
|
|
|
2020-10-06 04:34:40 +02:00
|
|
|
const firebaseSceneVersionCache = new WeakMap<SocketIOClient.Socket, number>();
|
|
|
|
|
|
|
|
export const isSavedToFirebase = (
|
|
|
|
portal: Portal,
|
|
|
|
elements: readonly ExcalidrawElement[],
|
|
|
|
): boolean => {
|
2020-11-29 18:32:51 +02:00
|
|
|
if (portal.socket && portal.roomId && portal.roomKey) {
|
2020-10-06 04:34:40 +02:00
|
|
|
const sceneVersion = getSceneVersion(elements);
|
|
|
|
return firebaseSceneVersionCache.get(portal.socket) === sceneVersion;
|
|
|
|
}
|
|
|
|
// if no room exists, consider the room saved so that we don't unnecessarily
|
2020-11-05 19:06:18 +02:00
|
|
|
// prevent unload (there's nothing we could do at that point anyway)
|
2020-10-06 04:34:40 +02:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
export const saveToFirebase = async (
|
2020-10-06 04:34:40 +02:00
|
|
|
portal: Portal,
|
2020-10-04 11:12:47 -07:00
|
|
|
elements: readonly ExcalidrawElement[],
|
2020-11-06 22:06:39 +02:00
|
|
|
) => {
|
2020-11-29 18:32:51 +02:00
|
|
|
const { roomId, roomKey, socket } = portal;
|
2020-10-06 04:34:40 +02:00
|
|
|
if (
|
|
|
|
// if no room exists, consider the room saved because there's nothing we can
|
2020-11-05 19:06:18 +02:00
|
|
|
// do at this point
|
2020-11-29 18:32:51 +02:00
|
|
|
!roomId ||
|
2020-10-06 04:34:40 +02:00
|
|
|
!roomKey ||
|
|
|
|
!socket ||
|
|
|
|
isSavedToFirebase(portal, elements)
|
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-10-04 11:12:47 -07:00
|
|
|
const firebase = await getFirebase();
|
|
|
|
const sceneVersion = getSceneVersion(elements);
|
2020-10-06 04:34:40 +02:00
|
|
|
const { ciphertext, iv } = await encryptElements(roomKey, elements);
|
2020-10-04 11:12:47 -07:00
|
|
|
|
|
|
|
const nextDocData = {
|
|
|
|
sceneVersion,
|
|
|
|
ciphertext: firebase.firestore.Blob.fromUint8Array(
|
|
|
|
new Uint8Array(ciphertext),
|
|
|
|
),
|
|
|
|
iv: firebase.firestore.Blob.fromUint8Array(iv),
|
|
|
|
} as FirebaseStoredScene;
|
|
|
|
|
|
|
|
const db = firebase.firestore();
|
2020-11-29 18:32:51 +02:00
|
|
|
const docRef = db.collection("scenes").doc(roomId);
|
2020-10-04 11:12:47 -07:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2020-10-06 04:34:40 +02:00
|
|
|
if (didUpdate) {
|
|
|
|
firebaseSceneVersionCache.set(socket, sceneVersion);
|
|
|
|
}
|
|
|
|
|
2020-10-04 11:12:47 -07:00
|
|
|
return didUpdate;
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-10-04 11:12:47 -07:00
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
export const loadFromFirebase = async (
|
2020-11-29 18:32:51 +02:00
|
|
|
roomId: string,
|
2020-10-06 04:34:40 +02:00
|
|
|
roomKey: string,
|
2020-11-06 22:06:39 +02:00
|
|
|
): Promise<readonly ExcalidrawElement[] | null> => {
|
2020-10-04 11:12:47 -07:00
|
|
|
const firebase = await getFirebase();
|
|
|
|
const db = firebase.firestore();
|
|
|
|
|
2020-11-29 18:32:51 +02:00
|
|
|
const docRef = db.collection("scenes").doc(roomId);
|
2020-10-04 11:12:47 -07:00
|
|
|
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();
|
2020-10-26 15:45:51 +01:00
|
|
|
return restoreElements(await decryptElements(roomKey, iv, ciphertext));
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|