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
File diff suppressed because it is too large Load Diff
+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; // IV is the Initialization Vector - random value to randomize the encryption output
const KEY_ALGO = { name: "AES-GCM", length: 256 }; // 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( export async function deriveMasterKey(
password: string, password: string,
email: string, email: string,
): Promise<CryptoKey> { ): Promise<CryptoKey> {
const encoder = new TextEncoder(); const enc = new TextEncoder();
const passwordKey = await crypto.subtle.importKey(
const baseKey = await crypto.subtle.importKey(
"raw", "raw",
encoder.encode(password), enc.encode(password),
"PBKDF2", "PBKDF2",
false, false,
["deriveKey"], ["deriveKey"],
@@ -24,12 +45,12 @@ export async function deriveMasterKey(
return crypto.subtle.deriveKey( return crypto.subtle.deriveKey(
{ {
name: "PBKDF2", name: "PBKDF2",
salt: encoder.encode(email.toLowerCase()), salt: enc.encode(email.toLowerCase()),
iterations: ITERATIONS, iterations: PBKDF2_ITERATIONS,
hash: "SHA-256", hash: "SHA-256",
}, },
passwordKey, baseKey,
KEY_ALGO, AES_GCM,
false, false,
["encrypt", "decrypt", "wrapKey", "unwrapKey"], ["encrypt", "decrypt", "wrapKey", "unwrapKey"],
); );
@@ -37,47 +58,44 @@ export async function deriveMasterKey(
/** /**
* Encrypts a letter using Envelope Encryption. * 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) { export async function encryptLetter(
const encoder = new TextEncoder(); plaintext: string,
masterKey: CryptoKey,
): Promise<EncryptedLetter> {
const enc = new TextEncoder();
// Generate random Data Encryption Key (DEK) // 1time DEK for this letter
const dek = await crypto.subtle.generateKey(KEY_ALGO, true, [ const dek = await crypto.subtle.generateKey(AES_GCM, true, [
"encrypt", "encrypt",
"decrypt", "decrypt",
]); ]);
// Encrypt the content with the DEK // encrypt the plaintext with the DEK
const iv = crypto.getRandomValues(new Uint8Array(12)); const contentIv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt( const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, { name: "AES-GCM", iv: contentIv },
dek, dek,
encoder.encode(plaintext), enc.encode(plaintext),
); );
// encrpyt the DEK using the Master Key for the self access // wrap the DEK with the Master Key (for self access)
const keyIv = crypto.getRandomValues(new Uint8Array(12)); const dekIv = crypto.getRandomValues(new Uint8Array(12));
const wrappedKey = await crypto.subtle.wrapKey("raw", dek, masterKey, { const wrappedDek = await crypto.subtle.wrapKey("raw", dek, masterKey, {
name: "AES-GCM", name: "AES-GCM",
iv: keyIv, iv: dekIv,
}); });
// for recipients (link share), export DEK in raw format // export raw DEK for the share URL (recipient access, no master key needed)
const rawKey = await crypto.subtle.exportKey("raw", dek); const rawDek = await crypto.subtle.exportKey("raw", dek);
// conversion to base64 for transit
const toBase64 = (buf: Uint8Array) =>
btoa(buf.reduce((acc, b) => acc + String.fromCharCode(b), ""));
return { return {
// This goes to the server encrypted_content: packWithIv(contentIv, ciphertext),
encryptedPayload: { encrypted_dek: packWithIv(dekIv, wrappedDek),
ciphertext: toBase64(new Uint8Array(ciphertext)), sharingKey: toBase64(new Uint8Array(rawDek)),
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)),
}; };
} }