integratedhub/helpers.ts
2026-01-20 09:02:26 -06:00

258 lines
7.1 KiB
TypeScript
Executable File

// 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<string> {
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<CryptoKey> {
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<string | null> {
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<string | null> {
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<string | null> {
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<string | null> {
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<string | null> {
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);
}