// Simple encryption utilities - only what's needed for the hook const ENCRYPTION_PASSWORD_KEY_PREFIX = "realestate_app"; const ENCRYPTION_SKIP_KEY_PREFIX = "realestate_app_skip"; // Generate user-specific localStorage key export function getUserEncryptionKey(userId: string): string { return `${ENCRYPTION_PASSWORD_KEY_PREFIX}_user_${userId}`; } // Generate user-specific skip key export function getUserSkipKey(userId: string): string { return `${ENCRYPTION_SKIP_KEY_PREFIX}_user_${userId}`; } export function MakeNewDate( input: string | number, timezone: string = "T00:00:00.000Z", ): Date { if (typeof input === "string") { if (input.length === 10) { // If it's a 10-character string (YYYY-MM-DD), append T00:00:00.000Z for consistent UTC midnight parsing const d = new Date(input + timezone); return d; } else if (input.length === 13) { // If it's a 13-character string, try to parse as timestamp const timestamp = parseInt(input, 10); if (isNaN(timestamp)) { throw new Error("Invalid 13-character timestamp string"); } const tDate = new Date(timestamp); const d = MakeNewDate(tDate.toISOString().split("T")[0]); return d; } else { throw new Error( "String input must be exactly 10 characters (YYYY-MM-DD) or 13 characters (timestamp)", ); } } else if (typeof input === "number") { // If it's a number (timestamp), use it directly const tDate = new Date(input); const d = MakeNewDate(tDate.toISOString().split("T")[0]); return d; } else { throw new Error("MakeNewDate expects a string or number input"); } } export function getDate(dateTime: Date): number { const d = MakeNewDate(dateTime.getTime()); d.setDate(d.getDate() + 1); return d.getDate(); } export function getMonth(dateTime: Date): number { const d = MakeNewDate(dateTime.getTime()); d.setDate(d.getDate() + 1); return d.getMonth(); } export function getYear(dateTime: Date): number { const d = MakeNewDate(dateTime.getTime()); d.setDate(d.getDate() + 1); return d.getFullYear(); } export function getDay(dateTime: Date): number { const d = MakeNewDate(dateTime.getTime()); d.setDate(d.getDate() + 1); return d.getDay(); } export async function createPublicId(guid: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(guid); const hashBuffer = await crypto.subtle.digest("SHA-256", data); return btoa(String.fromCharCode(...new Uint8Array(hashBuffer))) .replace(/[+/=]/g, "") .substring(0, 22); // Truncate for shorter ID } // Utility: derive an AES key from a password using PBKDF2 export async function deriveKey( password: string, salt: Uint8Array, ): Promise { const enc = new TextEncoder(); const passKey = await globalThis.crypto.subtle.importKey( "raw", enc.encode(password), "PBKDF2", false, ["deriveKey"], ); // Derive a 256-bit AES-GCM key return globalThis.crypto.subtle.deriveKey( { name: "PBKDF2", salt: new Uint8Array(salt), iterations: 100_000, hash: "SHA-256", }, passKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"], ); } // Utility: random salt/IV generation export function getRandomBytes(length: number): Uint8Array { const arr = new Uint8Array(length); globalThis.crypto.getRandomValues(arr); return arr; } // Encrypt data using password export async function encryptData( data: string, password: string, ): Promise { if (!data || !password) { return null; } try { const salt = getRandomBytes(16); const iv = getRandomBytes(12); const key = await deriveKey(password, salt); const enc = new TextEncoder().encode(data); const ciphertext = new Uint8Array( await globalThis.crypto.subtle.encrypt( { name: "AES-GCM", iv: new Uint8Array(iv) }, key, new Uint8Array(enc), ), ); // Bundle data as base64 const bundle = JSON.stringify({ salt: btoa(String.fromCharCode(...salt)), iv: btoa(String.fromCharCode(...iv)), data: btoa(String.fromCharCode(...ciphertext)), }); return bundle; } catch (error) { console.error("Encryption failed:", error); return null; } } // Decrypt data using password export async function decryptData( encryptedBundle: string, password: string, ): Promise { if (!encryptedBundle || !password) { return null; } try { const { salt, iv, data } = JSON.parse(encryptedBundle); const saltBytes = Uint8Array.from(atob(salt), (c) => c.charCodeAt(0)); const ivBytes = Uint8Array.from(atob(iv), (c) => c.charCodeAt(0)); const cipherBytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0)); const key = await deriveKey(password, saltBytes); const decryptedBytes = await globalThis.crypto.subtle.decrypt( { name: "AES-GCM", iv: new Uint8Array(ivBytes) }, key, new Uint8Array(cipherBytes), ); return new TextDecoder().decode(decryptedBytes); } catch (error) { console.error("Decryption failed:", error); return null; } } // Retrieve encryption password from localStorage export function getEncryptionPassword(userId: string): string | null { if (!userId) return null; try { const userKey = getUserEncryptionKey(userId); return globalThis.localStorage?.getItem(userKey) || null; } catch (error) { console.warn( "Failed to access localStorage for encryption password:", error, ); return null; } } // Encrypt data using stored password for a specific user (convenience function) export async function encryptDataWithStoredPassword( userId: string, data: string, ): Promise { if (!userId) { console.warn("Cannot encrypt data: userId is required"); return null; } const password = getEncryptionPassword(userId); if (!password) { console.warn(`No encryption password stored for user ${userId}`); return null; } const encryptedBundle = await encryptData(data, password); if (!encryptedBundle) { return null; } return btoa(encryptedBundle); } // Decrypt data using stored password for a specific user (convenience function) export async function decryptDataWithStoredPassword( userId: string, encryptedBundle: string, ): Promise { if (!userId) { console.warn("Cannot decrypt data: userId is required"); return null; } const password = getEncryptionPassword(userId); if (!password) { console.warn(`No encryption password stored for user ${userId}`); return null; } if (!encryptedBundle) { return null; } const decodedBundle = atob(encryptedBundle); return await decryptData(decodedBundle, password); } // Decrypt data using stored password for a specific user (convenience function) export async function tryDecryptDataWith( password: string, encryptedBundle: string, ): Promise { if (!password) { console.warn("Cannot decrypt data: password is required"); return null; } if (!encryptedBundle) { return null; } const decodedBundle = atob(encryptedBundle); return await decryptData(decodedBundle, password); }