mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
refactor: update crypto envelope encryption and add canvas serialization support
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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 }),
|
||||||
|
}));
|
||||||
@@ -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)),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user