// Helper functions for the application // Date utility functions export function MakeNewDate(input: string | number | Date): Date { if (input instanceof Date) { return input; } return new Date(input); } export function getDate(date: Date): number { return date.getDate(); } export function getMonth(date: Date): number { return date.getMonth(); } export function getYear(date: Date): number { return date.getFullYear(); } export function getDay(date: Date): number { return date.getDay(); } export function getDaysInBaseMonth(year: number, month: number): number { return new Date(year, month + 1, 0).getDate(); } // --------------------------------------------------------------------------- // Encryption: envelope format { salt, iv, data } (base64), AES-GCM + PBKDF2 // --------------------------------------------------------------------------- const PBKDF2_ITERATIONS = 100_000; const AES_KEY_LENGTH = 256; /** Decode base64 to a new Uint8Array (ArrayBuffer-backed for Web Crypto). */ function base64ToBytes(base64: string): Uint8Array { const bin = atob(base64); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return new Uint8Array(bytes); } function bytesToBase64(bytes: Uint8Array): string { let bin = ""; for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); return btoa(bin); } /** Decrypt AES-GCM ciphertext using key derived from password + salt. */ async function decryptEnvelope( password: string, envelope: { salt: string; iv: string; data: string }, ): Promise { const saltBytes = base64ToBytes(envelope.salt); const ivBytes = base64ToBytes(envelope.iv); const ciphertextBytes = base64ToBytes(envelope.data); const keyMaterial = await crypto.subtle.importKey( "raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"], ); const key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: saltBytes as BufferSource, iterations: PBKDF2_ITERATIONS, hash: "SHA-256", }, keyMaterial, { name: "AES-GCM", length: AES_KEY_LENGTH }, false, ["decrypt"], ); const plaintext = await crypto.subtle.decrypt( { name: "AES-GCM", iv: ivBytes as BufferSource }, key, ciphertextBytes as BufferSource, ); return new TextDecoder().decode(plaintext); } /** True if the decoded payload is an encrypted envelope we can decrypt. */ function isEnvelope( obj: unknown, ): obj is { salt: string; iv: string; data: string } { return ( typeof obj === "object" && obj !== null && "salt" in obj && "iv" in obj && "data" in obj && typeof (obj as { salt: unknown }).salt === "string" && typeof (obj as { iv: unknown }).iv === "string" && typeof (obj as { data: unknown }).data === "string" ); } export function encryptDataWithStoredPassword( _userId: string, data: string, ): string { // Legacy: plain base64 for backward compatibility when no envelope is used return btoa(data); } /** * Decrypt data using the PIN stored in localStorage for this user. * Supports: (1) encrypted envelope { salt, iv, data } then AES-GCM decrypt; * (2) legacy plain base64 string. */ export async function decryptDataWithStoredPassword( userId: string, encryptedData: string, ): Promise { const password = getEncryptionPassword(userId); if (!password) { console.log( "[Encryption] decryptDataWithStoredPassword: no PIN for userId, skipping decrypt", ); return ""; } try { const decoded = atob(encryptedData); let parsed: unknown; try { parsed = JSON.parse(decoded) as unknown; } catch { // Not JSON: treat as legacy plain base64 payload console.log( "[Encryption] decryptDataWithStoredPassword: legacy base64, returning decoded", ); return decoded; } if (isEnvelope(parsed)) { const out = await decryptEnvelope(password, parsed); console.log( "[Encryption] decryptDataWithStoredPassword: envelope decrypted ok", ); return out; } // JSON but not an envelope: legacy console.log( "[Encryption] decryptDataWithStoredPassword: JSON payload, returning as-is", ); return decoded; } catch (e) { console.log("[Encryption] decryptDataWithStoredPassword: failed", e); return ""; } } /** Decrypt using stored PIN (from localStorage by userId). Returns null on failure. */ export async function tryDecryptDataWith( userId: string, encryptedData: string, ): Promise { try { const out = await decryptDataWithStoredPassword(userId, encryptedData); return out || null; } catch { return null; } } /** Decrypt using a raw PIN string (e.g. from server sync). Supports envelope or legacy base64. */ export async function tryDecryptDataWithPin( pin: string, encryptedData: string, ): Promise { if (!pin) return null; try { const decoded = atob(encryptedData); let parsed: unknown; try { parsed = JSON.parse(decoded) as unknown; } catch { return decoded; } if (isEnvelope(parsed)) { return await decryptEnvelope(pin, parsed); } return decoded; } catch { return null; } } const ENCRYPTION_KEY_PREFIX = "encryption_password_"; export function getEncryptionPassword(userId: string): string | null { if (!userId) { console.log("[Encryption] getEncryptionPassword: no userId"); return null; } try { const pin = globalThis.localStorage?.getItem( ENCRYPTION_KEY_PREFIX + userId, ); console.log( "[Encryption] getEncryptionPassword:", pin ? "PIN available" : "no PIN", "key=" + ENCRYPTION_KEY_PREFIX + userId, ); return pin; } catch { return null; } } /** Store encryption PIN in localStorage for this user (used by WelcomeBar and calendar). */ export function setEncryptionPassword( userId: string, password: string, ): boolean { if (!userId) { console.log("[Encryption] setEncryptionPassword: no userId, skip"); return false; } try { const key = getUserEncryptionKey(userId); if (key) globalThis.localStorage?.setItem(key, password); console.log("[Encryption] setEncryptionPassword: stored for key=" + key); return true; } catch (e) { console.log("[Encryption] setEncryptionPassword: failed", e); return false; } } export function getUserEncryptionKey(userId: string): string | null { if (!userId) return null; return ENCRYPTION_KEY_PREFIX + userId; } export function getUserSkipKey(userId: string): string { return `skip_encryption_${userId}`; } export function getUserSkipValue(userId: string): boolean { const skipKey = getUserSkipKey(userId); try { return globalThis.localStorage?.getItem(skipKey) === "true"; } catch { return false; } } export function setUserSkipKey(userId: string, skip: boolean): void { const skipKey = `skip_encryption_${userId}`; try { if (skip) { globalThis.localStorage?.setItem(skipKey, "true"); } else { globalThis.localStorage?.removeItem(skipKey); } } catch { // Ignore localStorage errors } } export function createPublicId(userId: string): string { // Create a public ID from user ID (simple implementation) return btoa(userId).replace(/[+/=]/g, "").substring(0, 10); }