refactor: deduplicate encryption helpers (#4146)
This commit is contained in:
parent
f59e608f18
commit
6143d5195a
@ -174,3 +174,5 @@ export const ALLOWED_IMAGE_MIME_TYPES = [
|
|||||||
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
export const ENCRYPTION_KEY_BITS = 128;
|
||||||
|
@ -11,6 +11,7 @@ import { CanvasError } from "../errors";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { AppState, DataURL } from "../types";
|
import { AppState, DataURL } from "../types";
|
||||||
|
import { bytesToHexString } from "../utils";
|
||||||
import { FileSystemHandle } from "./filesystem";
|
import { FileSystemHandle } from "./filesystem";
|
||||||
import { isValidExcalidrawData } from "./json";
|
import { isValidExcalidrawData } from "./json";
|
||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
@ -195,26 +196,18 @@ export const canvasToBlob = async (
|
|||||||
|
|
||||||
/** generates SHA-1 digest from supplied file (if not supported, falls back
|
/** generates SHA-1 digest from supplied file (if not supported, falls back
|
||||||
to a 40-char base64 random id) */
|
to a 40-char base64 random id) */
|
||||||
export const generateIdFromFile = async (file: File) => {
|
export const generateIdFromFile = async (file: File): Promise<FileId> => {
|
||||||
let id: FileId;
|
|
||||||
try {
|
try {
|
||||||
const hashBuffer = await window.crypto.subtle.digest(
|
const hashBuffer = await window.crypto.subtle.digest(
|
||||||
"SHA-1",
|
"SHA-1",
|
||||||
await file.arrayBuffer(),
|
await file.arrayBuffer(),
|
||||||
);
|
);
|
||||||
id =
|
return bytesToHexString(new Uint8Array(hashBuffer)) as FileId;
|
||||||
// convert buffer to byte array
|
|
||||||
Array.from(new Uint8Array(hashBuffer))
|
|
||||||
// convert to hex string
|
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
||||||
.join("") as FileId;
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
// length 40 to align with the HEX length of SHA-1 (which is 160 bit)
|
// length 40 to align with the HEX length of SHA-1 (which is 160 bit)
|
||||||
id = nanoid(40) as FileId;
|
return nanoid(40) as FileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return id;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDataURL = async (file: Blob | File): Promise<DataURL> => {
|
export const getDataURL = async (file: Blob | File): Promise<DataURL> => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ENCRYPTION_KEY_BITS } from "../constants";
|
||||||
|
|
||||||
export const IV_LENGTH_BYTES = 12;
|
export const IV_LENGTH_BYTES = 12;
|
||||||
|
|
||||||
export const createIV = () => {
|
export const createIV = () => {
|
||||||
@ -5,19 +7,27 @@ export const createIV = () => {
|
|||||||
return window.crypto.getRandomValues(arr);
|
return window.crypto.getRandomValues(arr);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateEncryptionKey = async () => {
|
export const generateEncryptionKey = async <
|
||||||
|
T extends "string" | "cryptoKey" = "string",
|
||||||
|
>(
|
||||||
|
returnAs?: T,
|
||||||
|
): Promise<T extends "cryptoKey" ? CryptoKey : string> => {
|
||||||
const key = await window.crypto.subtle.generateKey(
|
const key = await window.crypto.subtle.generateKey(
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
length: 128,
|
length: ENCRYPTION_KEY_BITS,
|
||||||
},
|
},
|
||||||
true, // extractable
|
true, // extractable
|
||||||
["encrypt", "decrypt"],
|
["encrypt", "decrypt"],
|
||||||
);
|
);
|
||||||
return (await window.crypto.subtle.exportKey("jwk", key)).k;
|
return (
|
||||||
|
returnAs === "cryptoKey"
|
||||||
|
? key
|
||||||
|
: (await window.crypto.subtle.exportKey("jwk", key)).k
|
||||||
|
) as T extends "cryptoKey" ? CryptoKey : string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImportedKey = (key: string, usage: KeyUsage) =>
|
export const getCryptoKey = (key: string, usage: KeyUsage) =>
|
||||||
window.crypto.subtle.importKey(
|
window.crypto.subtle.importKey(
|
||||||
"jwk",
|
"jwk",
|
||||||
{
|
{
|
||||||
@ -29,17 +39,18 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
length: 128,
|
length: ENCRYPTION_KEY_BITS,
|
||||||
},
|
},
|
||||||
false, // extractable
|
false, // extractable
|
||||||
[usage],
|
[usage],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const encryptData = async (
|
export const encryptData = async (
|
||||||
key: string,
|
key: string | CryptoKey,
|
||||||
data: Uint8Array | ArrayBuffer | Blob | File | string,
|
data: Uint8Array | ArrayBuffer | Blob | File | string,
|
||||||
): Promise<{ encryptedBuffer: ArrayBuffer; iv: Uint8Array }> => {
|
): Promise<{ encryptedBuffer: ArrayBuffer; iv: Uint8Array }> => {
|
||||||
const importedKey = await getImportedKey(key, "encrypt");
|
const importedKey =
|
||||||
|
typeof key === "string" ? await getCryptoKey(key, "encrypt") : key;
|
||||||
const iv = createIV();
|
const iv = createIV();
|
||||||
const buffer: ArrayBuffer | Uint8Array =
|
const buffer: ArrayBuffer | Uint8Array =
|
||||||
typeof data === "string"
|
typeof data === "string"
|
||||||
@ -50,6 +61,8 @@ export const encryptData = async (
|
|||||||
? await data.arrayBuffer()
|
? await data.arrayBuffer()
|
||||||
: data;
|
: data;
|
||||||
|
|
||||||
|
// We use symmetric encryption. AES-GCM is the recommended algorithm and
|
||||||
|
// includes checks that the ciphertext has not been modified by an attacker.
|
||||||
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
@ -67,7 +80,7 @@ export const decryptData = async (
|
|||||||
encrypted: Uint8Array | ArrayBuffer,
|
encrypted: Uint8Array | ArrayBuffer,
|
||||||
privateKey: string,
|
privateKey: string,
|
||||||
): Promise<ArrayBuffer> => {
|
): Promise<ArrayBuffer> => {
|
||||||
const key = await getImportedKey(privateKey, "decrypt");
|
const key = await getCryptoKey(privateKey, "decrypt");
|
||||||
return window.crypto.subtle.decrypt(
|
return window.crypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
|
@ -23,3 +23,5 @@ export const FIREBASE_STORAGE_PREFIXES = {
|
|||||||
shareLinkFiles: `/files/shareLinks`,
|
shareLinkFiles: `/files/shareLinks`,
|
||||||
collabFiles: `/files/rooms`,
|
collabFiles: `/files/rooms`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ROOM_ID_BYTES = 10;
|
||||||
|
@ -24,7 +24,6 @@ import {
|
|||||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import {
|
import {
|
||||||
decryptAESGEM,
|
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
@ -65,6 +64,7 @@ import {
|
|||||||
ReconciledElements,
|
ReconciledElements,
|
||||||
reconcileElements as _reconcileElements,
|
reconcileElements as _reconcileElements,
|
||||||
} from "./reconciliation";
|
} from "./reconciliation";
|
||||||
|
import { decryptData } from "../../data/encryption";
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
modalIsShown: boolean;
|
modalIsShown: boolean;
|
||||||
@ -301,6 +301,27 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
return await this.fileManager.getFiles(unfetchedImages);
|
return await this.fileManager.getFiles(unfetchedImages);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private decryptPayload = async (
|
||||||
|
iv: Uint8Array,
|
||||||
|
encryptedData: ArrayBuffer,
|
||||||
|
decryptionKey: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const decrypted = await decryptData(iv, encryptedData, decryptionKey);
|
||||||
|
|
||||||
|
const decodedData = new TextDecoder("utf-8").decode(
|
||||||
|
new Uint8Array(decrypted),
|
||||||
|
);
|
||||||
|
return JSON.parse(decodedData);
|
||||||
|
} catch (error) {
|
||||||
|
window.alert(t("alerts.decryptFailed"));
|
||||||
|
console.error(error);
|
||||||
|
return {
|
||||||
|
type: "INVALID_RESPONSE",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private initializeSocketClient = async (
|
private initializeSocketClient = async (
|
||||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||||
): Promise<ImportedDataState | null> => {
|
): Promise<ImportedDataState | null> => {
|
||||||
@ -388,10 +409,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
if (!this.portal.roomKey) {
|
if (!this.portal.roomKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const decryptedData = await decryptAESGEM(
|
|
||||||
|
const decryptedData = await this.decryptPayload(
|
||||||
|
iv,
|
||||||
encryptedData,
|
encryptedData,
|
||||||
this.portal.roomKey,
|
this.portal.roomKey,
|
||||||
iv,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (decryptedData.type) {
|
switch (decryptedData.type) {
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { SocketUpdateData, SocketUpdateDataSource } from "../data";
|
||||||
encryptAESGEM,
|
|
||||||
SocketUpdateData,
|
|
||||||
SocketUpdateDataSource,
|
|
||||||
} from "../data";
|
|
||||||
|
|
||||||
import CollabWrapper from "./CollabWrapper";
|
import CollabWrapper from "./CollabWrapper";
|
||||||
|
|
||||||
@ -13,6 +9,7 @@ import { trackEvent } from "../../analytics";
|
|||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import { newElementWith } from "../../element/mutateElement";
|
import { newElementWith } from "../../element/mutateElement";
|
||||||
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||||
|
import { encryptData } from "../../data/encryption";
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: CollabWrapper;
|
collab: CollabWrapper;
|
||||||
@ -79,12 +76,13 @@ class Portal {
|
|||||||
if (this.isOpen()) {
|
if (this.isOpen()) {
|
||||||
const json = JSON.stringify(data);
|
const json = JSON.stringify(data);
|
||||||
const encoded = new TextEncoder().encode(json);
|
const encoded = new TextEncoder().encode(json);
|
||||||
const encrypted = await encryptAESGEM(encoded, this.roomKey!);
|
const { encryptedBuffer, iv } = await encryptData(this.roomKey!, encoded);
|
||||||
|
|
||||||
this.socket?.emit(
|
this.socket?.emit(
|
||||||
volatile ? BROADCAST.SERVER_VOLATILE : BROADCAST.SERVER,
|
volatile ? BROADCAST.SERVER_VOLATILE : BROADCAST.SERVER,
|
||||||
this.roomId,
|
this.roomId,
|
||||||
encrypted.data,
|
encryptedBuffer,
|
||||||
encrypted.iv,
|
iv,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { restoreElements } from "../../data/restore";
|
|||||||
import { BinaryFileData, BinaryFileMetadata, DataURL } from "../../types";
|
import { BinaryFileData, BinaryFileMetadata, DataURL } from "../../types";
|
||||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
||||||
import { decompressData } from "../../data/encode";
|
import { decompressData } from "../../data/encode";
|
||||||
import { getImportedKey, createIV } from "../../data/encryption";
|
import { encryptData, decryptData } from "../../data/encryption";
|
||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../constants";
|
||||||
|
|
||||||
// private
|
// private
|
||||||
@ -92,20 +92,11 @@ const encryptElements = async (
|
|||||||
key: string,
|
key: string,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> => {
|
): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> => {
|
||||||
const importedKey = await getImportedKey(key, "encrypt");
|
|
||||||
const iv = createIV();
|
|
||||||
const json = JSON.stringify(elements);
|
const json = JSON.stringify(elements);
|
||||||
const encoded = new TextEncoder().encode(json);
|
const encoded = new TextEncoder().encode(json);
|
||||||
const ciphertext = await window.crypto.subtle.encrypt(
|
const { encryptedBuffer, iv } = await encryptData(key, encoded);
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv,
|
|
||||||
},
|
|
||||||
importedKey,
|
|
||||||
encoded,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ciphertext, iv };
|
return { ciphertext: encryptedBuffer, iv };
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptElements = async (
|
const decryptElements = async (
|
||||||
@ -113,16 +104,7 @@ const decryptElements = async (
|
|||||||
iv: Uint8Array,
|
iv: Uint8Array,
|
||||||
ciphertext: ArrayBuffer | Uint8Array,
|
ciphertext: ArrayBuffer | Uint8Array,
|
||||||
): Promise<readonly ExcalidrawElement[]> => {
|
): Promise<readonly ExcalidrawElement[]> => {
|
||||||
const importedKey = await getImportedKey(key, "decrypt");
|
const decrypted = await decryptData(iv, ciphertext, key);
|
||||||
const decrypted = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv,
|
|
||||||
},
|
|
||||||
importedKey,
|
|
||||||
ciphertext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const decodedData = new TextDecoder("utf-8").decode(
|
const decodedData = new TextDecoder("utf-8").decode(
|
||||||
new Uint8Array(decrypted),
|
new Uint8Array(decrypted),
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createIV,
|
decryptData,
|
||||||
|
encryptData,
|
||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
getImportedKey,
|
|
||||||
IV_LENGTH_BYTES,
|
IV_LENGTH_BYTES,
|
||||||
} from "../../data/encryption";
|
} from "../../data/encryption";
|
||||||
import { serializeAsJSON } from "../../data/json";
|
import { serializeAsJSON } from "../../data/json";
|
||||||
@ -16,19 +16,18 @@ import {
|
|||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
import { bytesToHexString } from "../../utils";
|
||||||
|
import { FILE_UPLOAD_MAX_BYTES, ROOM_ID_BYTES } from "../app_constants";
|
||||||
import { encodeFilesForUpload } from "./FileManager";
|
import { encodeFilesForUpload } from "./FileManager";
|
||||||
import { saveFilesToFirebase } from "./firebase";
|
import { saveFilesToFirebase } from "./firebase";
|
||||||
|
|
||||||
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
|
|
||||||
|
|
||||||
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
|
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
|
||||||
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
|
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
|
||||||
|
|
||||||
const generateRandomID = async () => {
|
const generateRoomId = async () => {
|
||||||
const arr = new Uint8Array(10);
|
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
||||||
window.crypto.getRandomValues(arr);
|
window.crypto.getRandomValues(buffer);
|
||||||
return Array.from(arr, byteToHex).join("");
|
return bytesToHexString(buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SOCKET_SERVER = process.env.REACT_APP_SOCKET_SERVER_URL;
|
export const SOCKET_SERVER = process.env.REACT_APP_SOCKET_SERVER_URL;
|
||||||
@ -82,54 +81,6 @@ export type SocketUpdateData =
|
|||||||
_brand: "socketUpdateData";
|
_brand: "socketUpdateData";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encryptAESGEM = async (
|
|
||||||
data: Uint8Array,
|
|
||||||
key: string,
|
|
||||||
): Promise<EncryptedData> => {
|
|
||||||
const importedKey = await getImportedKey(key, "encrypt");
|
|
||||||
const iv = createIV();
|
|
||||||
return {
|
|
||||||
data: await window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv,
|
|
||||||
},
|
|
||||||
importedKey,
|
|
||||||
data,
|
|
||||||
),
|
|
||||||
iv,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decryptAESGEM = async (
|
|
||||||
data: ArrayBuffer,
|
|
||||||
key: string,
|
|
||||||
iv: Uint8Array,
|
|
||||||
): Promise<SocketUpdateDataIncoming> => {
|
|
||||||
try {
|
|
||||||
const importedKey = await getImportedKey(key, "decrypt");
|
|
||||||
const decrypted = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv,
|
|
||||||
},
|
|
||||||
importedKey,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
const decodedData = new TextDecoder("utf-8").decode(
|
|
||||||
new Uint8Array(decrypted),
|
|
||||||
);
|
|
||||||
return JSON.parse(decodedData);
|
|
||||||
} catch (error: any) {
|
|
||||||
window.alert(t("alerts.decryptFailed"));
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: "INVALID_RESPONSE",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCollaborationLinkData = (link: string) => {
|
export const getCollaborationLinkData = (link: string) => {
|
||||||
const hash = new URL(link).hash;
|
const hash = new URL(link).hash;
|
||||||
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
||||||
@ -141,7 +92,7 @@ export const getCollaborationLinkData = (link: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateCollaborationLinkData = async () => {
|
export const generateCollaborationLinkData = async () => {
|
||||||
const roomId = await generateRandomID();
|
const roomId = await generateRoomId();
|
||||||
const roomKey = await generateEncryptionKey();
|
const roomKey = await generateEncryptionKey();
|
||||||
|
|
||||||
if (!roomKey) {
|
if (!roomKey) {
|
||||||
@ -158,22 +109,6 @@ export const getCollaborationLink = (data: {
|
|||||||
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
|
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decryptImported = async (
|
|
||||||
iv: ArrayBuffer | Uint8Array,
|
|
||||||
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 (
|
const importFromBackend = async (
|
||||||
id: string,
|
id: string,
|
||||||
privateKey: string,
|
privateKey: string,
|
||||||
@ -192,11 +127,11 @@ const importFromBackend = async (
|
|||||||
// Buffer should contain both the IV (fixed length) and encrypted data
|
// Buffer should contain both the IV (fixed length) and encrypted data
|
||||||
const iv = buffer.slice(0, IV_LENGTH_BYTES);
|
const iv = buffer.slice(0, IV_LENGTH_BYTES);
|
||||||
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
|
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
|
||||||
decrypted = await decryptImported(iv, encrypted, privateKey);
|
decrypted = await decryptData(new Uint8Array(iv), encrypted, privateKey);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Fixed IV (old format, backward compatibility)
|
// Fixed IV (old format, backward compatibility)
|
||||||
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
|
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
|
||||||
decrypted = await decryptImported(fixedIv, buffer, privateKey);
|
decrypted = await decryptData(fixedIv, buffer, privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to convert the decrypted array buffer to a string
|
// We need to convert the decrypted array buffer to a string
|
||||||
@ -256,29 +191,12 @@ export const exportToBackend = async (
|
|||||||
const json = serializeAsJSON(elements, appState, files, "database");
|
const json = serializeAsJSON(elements, appState, files, "database");
|
||||||
const encoded = new TextEncoder().encode(json);
|
const encoded = new TextEncoder().encode(json);
|
||||||
|
|
||||||
const cryptoKey = await window.crypto.subtle.generateKey(
|
const cryptoKey = await generateEncryptionKey("cryptoKey");
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
length: 128,
|
|
||||||
},
|
|
||||||
true, // extractable
|
|
||||||
["encrypt", "decrypt"],
|
|
||||||
);
|
|
||||||
|
|
||||||
const iv = createIV();
|
const { encryptedBuffer, iv } = await encryptData(cryptoKey, encoded);
|
||||||
// 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(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv,
|
|
||||||
},
|
|
||||||
cryptoKey,
|
|
||||||
encoded,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Concatenate IV with encrypted data (IV does not have to be secret).
|
// Concatenate IV with encrypted data (IV does not have to be secret).
|
||||||
const payloadBlob = new Blob([iv.buffer, encrypted]);
|
const payloadBlob = new Blob([iv.buffer, encryptedBuffer]);
|
||||||
const payload = await new Response(payloadBlob).arrayBuffer();
|
const payload = await new Response(payloadBlob).arrayBuffer();
|
||||||
|
|
||||||
// We use jwk encoding to be able to extract just the base64 encoded key.
|
// We use jwk encoding to be able to extract just the base64 encoded key.
|
||||||
|
@ -449,3 +449,9 @@ export const preventUnload = (event: BeforeUnloadEvent) => {
|
|||||||
// NOTE: modern browsers no longer allow showing a custom message here
|
// NOTE: modern browsers no longer allow showing a custom message here
|
||||||
event.returnValue = "";
|
event.returnValue = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const bytesToHexString = (bytes: Uint8Array) => {
|
||||||
|
return Array.from(bytes)
|
||||||
|
.map((byte) => `0${byte.toString(16)}`.slice(-2))
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user