1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-10 11:35:52 +01:00

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)
This commit is contained in:
Mike Kowalski 2021-03-22 06:31:35 +01:00 committed by GitHub
parent 127c1be6ad
commit c8743a8c02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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) {