feat: compress shareLink data when uploading to json server (#4225)

This commit is contained in:
David Luzar 2021-11-24 14:45:13 +01:00 committed by GitHub
parent 133ba19919
commit 896c476716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 78 additions and 38 deletions

View File

@ -234,7 +234,19 @@ const splitBuffers = (concatenatedBuffer: Uint8Array) => {
let cursor = 0; let cursor = 0;
// first chunk is the version (ignored for now) // first chunk is the version
const version = dataView(
concatenatedBuffer,
NEXT_CHUNK_SIZE_DATAVIEW_BYTES,
cursor,
);
// If version is outside of the supported versions, throw an error.
// This usually means the buffer wasn't encoded using this API, so we'd only
// waste compute.
if (version > CONCAT_BUFFERS_VERSION) {
throw new Error(`invalid version ${version}`);
}
cursor += VERSION_DATAVIEW_BYTES; cursor += VERSION_DATAVIEW_BYTES;
while (true) { while (true) {

View File

@ -1,6 +1,6 @@
import { compressData, decompressData } from "../../data/encode";
import { import {
decryptData, decryptData,
encryptData,
generateEncryptionKey, generateEncryptionKey,
IV_LENGTH_BYTES, IV_LENGTH_BYTES,
} from "../../data/encryption"; } from "../../data/encryption";
@ -109,9 +109,45 @@ 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}`;
}; };
/**
* Decodes shareLink data using the legacy buffer format.
* @deprecated
*/
const legacy_decodeFromBackend = async ({
buffer,
decryptionKey,
}: {
buffer: ArrayBuffer;
decryptionKey: string;
}) => {
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 decryptData(new Uint8Array(iv), encrypted, decryptionKey);
} catch (error: any) {
// Fixed IV (old format, backward compatibility)
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
decrypted = await decryptData(fixedIv, buffer, decryptionKey);
}
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted),
);
const data: ImportedDataState = JSON.parse(string);
return {
elements: data.elements || null,
appState: data.appState || null,
};
};
const importFromBackend = async ( const importFromBackend = async (
id: string, id: string,
privateKey: string, decryptionKey: string,
): Promise<ImportedDataState> => { ): Promise<ImportedDataState> => {
try { try {
const response = await fetch(`${BACKEND_V2_GET}${id}`); const response = await fetch(`${BACKEND_V2_GET}${id}`);
@ -122,28 +158,28 @@ const importFromBackend = async (
} }
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
let decrypted: ArrayBuffer;
try { try {
// Buffer should contain both the IV (fixed length) and encrypted data const { data: decodedBuffer } = await decompressData(
const iv = buffer.slice(0, IV_LENGTH_BYTES); new Uint8Array(buffer),
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength); {
decrypted = await decryptData(new Uint8Array(iv), encrypted, privateKey); decryptionKey,
},
);
const data: ImportedDataState = JSON.parse(
new TextDecoder().decode(decodedBuffer),
);
return {
elements: data.elements || null,
appState: data.appState || null,
};
} catch (error: any) { } catch (error: any) {
// Fixed IV (old format, backward compatibility) console.warn(
const fixedIv = new Uint8Array(IV_LENGTH_BYTES); "error when decoding shareLink data using the new format:",
decrypted = await decryptData(fixedIv, buffer, privateKey); error,
);
return legacy_decodeFromBackend({ buffer, decryptionKey });
} }
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted),
);
const data: ImportedDataState = JSON.parse(string);
return {
elements: data.elements || null,
appState: data.appState || null,
};
} catch (error: any) { } catch (error: any) {
window.alert(t("alerts.importBackendFailed")); window.alert(t("alerts.importBackendFailed"));
console.error(error); console.error(error);
@ -188,20 +224,14 @@ export const exportToBackend = async (
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
const json = serializeAsJSON(elements, appState, files, "database"); const encryptionKey = await generateEncryptionKey("string");
const encoded = new TextEncoder().encode(json);
const cryptoKey = await generateEncryptionKey("cryptoKey"); const payload = await compressData(
new TextEncoder().encode(
const { encryptedBuffer, iv } = await encryptData(cryptoKey, encoded); serializeAsJSON(elements, appState, files, "database"),
),
// Concatenate IV with encrypted data (IV does not have to be secret). { encryptionKey },
const payloadBlob = new Blob([iv.buffer, encryptedBuffer]); );
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", cryptoKey);
try { try {
const filesMap = new Map<FileId, BinaryFileData>(); const filesMap = new Map<FileId, BinaryFileData>();
@ -211,8 +241,6 @@ export const exportToBackend = async (
} }
} }
const encryptionKey = exportedKey.k!;
const filesToUpload = await encodeFilesForUpload({ const filesToUpload = await encodeFilesForUpload({
files: filesMap, files: filesMap,
encryptionKey, encryptionKey,
@ -221,7 +249,7 @@ export const exportToBackend = async (
const response = await fetch(BACKEND_V2_POST, { const response = await fetch(BACKEND_V2_POST, {
method: "POST", method: "POST",
body: payload, body: payload.buffer,
}); });
const json = await response.json(); const json = await response.json();
if (json.id) { if (json.id) {