refactor: deduplicate encryption helpers (#4146)

This commit is contained in:
David Luzar 2021-11-07 14:33:21 +01:00 committed by GitHub
parent f59e608f18
commit 6143d5195a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 84 additions and 148 deletions

View file

@ -11,6 +11,7 @@ import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState, DataURL } from "../types";
import { bytesToHexString } from "../utils";
import { FileSystemHandle } from "./filesystem";
import { isValidExcalidrawData } from "./json";
import { restore } from "./restore";
@ -195,26 +196,18 @@ export const canvasToBlob = async (
/** generates SHA-1 digest from supplied file (if not supported, falls back
to a 40-char base64 random id) */
export const generateIdFromFile = async (file: File) => {
let id: FileId;
export const generateIdFromFile = async (file: File): Promise<FileId> => {
try {
const hashBuffer = await window.crypto.subtle.digest(
"SHA-1",
await file.arrayBuffer(),
);
id =
// 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;
return bytesToHexString(new Uint8Array(hashBuffer)) as FileId;
} catch (error: any) {
console.error(error);
// 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> => {

View file

@ -1,3 +1,5 @@
import { ENCRYPTION_KEY_BITS } from "../constants";
export const IV_LENGTH_BYTES = 12;
export const createIV = () => {
@ -5,19 +7,27 @@ export const createIV = () => {
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(
{
name: "AES-GCM",
length: 128,
length: ENCRYPTION_KEY_BITS,
},
true, // extractable
["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(
"jwk",
{
@ -29,17 +39,18 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
},
{
name: "AES-GCM",
length: 128,
length: ENCRYPTION_KEY_BITS,
},
false, // extractable
[usage],
);
export const encryptData = async (
key: string,
key: string | CryptoKey,
data: Uint8Array | ArrayBuffer | Blob | File | string,
): 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 buffer: ArrayBuffer | Uint8Array =
typeof data === "string"
@ -50,6 +61,8 @@ export const encryptData = async (
? await data.arrayBuffer()
: 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(
{
name: "AES-GCM",
@ -67,7 +80,7 @@ export const decryptData = async (
encrypted: Uint8Array | ArrayBuffer,
privateKey: string,
): Promise<ArrayBuffer> => {
const key = await getImportedKey(privateKey, "decrypt");
const key = await getCryptoKey(privateKey, "decrypt");
return window.crypto.subtle.decrypt(
{
name: "AES-GCM",