refactor: update crypto envelope encryption and add canvas serialization support

This commit is contained in:
Your Name
2026-04-11 20:11:13 +05:30
parent 73a9787daf
commit cc9301740c
3 changed files with 1426 additions and 38 deletions
+12
View File
@@ -0,0 +1,12 @@
import { create } from "zustand";
interface KeyState {
masterKey: CryptoKey | null;
setMasterKey: (key: CryptoKey | null) => void;
}
// this key will be used to encrypt and decrypt the user's data
export const useKeyStore = create<KeyState>((set) => ({
masterKey: null,
setMasterKey: (masterKey) => set({ masterKey }),
}));
+56 -38
View File
@@ -1,21 +1,42 @@
/**
* 0 knowledge cryptography. No Server involved in encryption/decryption
* 0 knowledge cryptography — no server involvement in encryption/decryption.
*/
const ITERATIONS = 100000;
const KEY_ALGO = { name: "AES-GCM", length: 256 };
// IV is the Initialization Vector - random value to randomize the encryption output
// DEK is the Data Encryption Key - random value to encrypt the plaintext
export interface EncryptedLetter {
encrypted_content: string; // IV + ciphertext, base64
encrypted_dek: string; // IV + wrapped DEK, base64
sharingKey: string; // raw DEK, base64 (embedded in share URL)
}
const PBKDF2_ITERATIONS = 100_000;
const AES_GCM = { name: "AES-GCM", length: 256 } as const;
const toBase64 = (buf: Uint8Array): string =>
btoa(buf.reduce((s, b) => s + String.fromCharCode(b), ""));
// Prefix the IV to data and base64-encode the result.
const packWithIv = (iv: Uint8Array, data: ArrayBuffer): string => {
const packed = new Uint8Array(iv.length + data.byteLength);
packed.set(iv);
packed.set(new Uint8Array(data), iv.length);
return toBase64(packed);
};
/**
* Derives a Master Encryption Key from a password and email (salt).
* Derives a Master Key from the user's password and email (used as PBKDF2 salt).
* Note: it is deterministic, i.e. the same credentials always produce the same key
*/
export async function deriveMasterKey(
password: string,
email: string,
): Promise<CryptoKey> {
const encoder = new TextEncoder();
const passwordKey = await crypto.subtle.importKey(
const enc = new TextEncoder();
const baseKey = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
enc.encode(password),
"PBKDF2",
false,
["deriveKey"],
@@ -24,12 +45,12 @@ export async function deriveMasterKey(
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: encoder.encode(email.toLowerCase()),
iterations: ITERATIONS,
salt: enc.encode(email.toLowerCase()),
iterations: PBKDF2_ITERATIONS,
hash: "SHA-256",
},
passwordKey,
KEY_ALGO,
baseKey,
AES_GCM,
false,
["encrypt", "decrypt", "wrapKey", "unwrapKey"],
);
@@ -37,47 +58,44 @@ export async function deriveMasterKey(
/**
* Encrypts a letter using Envelope Encryption.
*
* plaintext >> DEK >> encrypted_content
* DEK >> masterKey >> encrypted_dek
* DEK >> raw >> sharingKey
*/
export async function encryptLetter(plaintext: string, masterKey: CryptoKey) {
const encoder = new TextEncoder();
export async function encryptLetter(
plaintext: string,
masterKey: CryptoKey,
): Promise<EncryptedLetter> {
const enc = new TextEncoder();
// Generate random Data Encryption Key (DEK)
const dek = await crypto.subtle.generateKey(KEY_ALGO, true, [
// 1time DEK for this letter
const dek = await crypto.subtle.generateKey(AES_GCM, true, [
"encrypt",
"decrypt",
]);
// Encrypt the content with the DEK
const iv = crypto.getRandomValues(new Uint8Array(12));
// encrypt the plaintext with the DEK
const contentIv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
{ name: "AES-GCM", iv: contentIv },
dek,
encoder.encode(plaintext),
enc.encode(plaintext),
);
// encrpyt the DEK using the Master Key for the self access
const keyIv = crypto.getRandomValues(new Uint8Array(12));
const wrappedKey = await crypto.subtle.wrapKey("raw", dek, masterKey, {
// wrap the DEK with the Master Key (for self access)
const dekIv = crypto.getRandomValues(new Uint8Array(12));
const wrappedDek = await crypto.subtle.wrapKey("raw", dek, masterKey, {
name: "AES-GCM",
iv: keyIv,
iv: dekIv,
});
// for recipients (link share), export DEK in raw format
const rawKey = await crypto.subtle.exportKey("raw", dek);
// conversion to base64 for transit
const toBase64 = (buf: Uint8Array) =>
btoa(buf.reduce((acc, b) => acc + String.fromCharCode(b), ""));
// export raw DEK for the share URL (recipient access, no master key needed)
const rawDek = await crypto.subtle.exportKey("raw", dek);
return {
// This goes to the server
encryptedPayload: {
ciphertext: toBase64(new Uint8Array(ciphertext)),
iv: toBase64(new Uint8Array(iv)),
wrappedKey: toBase64(new Uint8Array(wrappedKey)),
keyIv: toBase64(new Uint8Array(keyIv)),
},
// This goes into the url for the recipient
sharingKey: toBase64(new Uint8Array(rawKey)),
encrypted_content: packWithIv(contentIv, ciphertext),
encrypted_dek: packWithIv(dekIv, wrappedDek),
sharingKey: toBase64(new Uint8Array(rawDek)),
};
}