// AES Encryption wrapper using Web Crypto API // In a real mobile app, this should be backed by the OS Keychain (via Capacitor Secure Storage). // For now, we generate a master key and store it in localStorage if it doesn't exist. function isCryptoAvailable() { return window.crypto && window.crypto.subtle; } // Fallback for non-secure contexts (HTTP via local IP on mobile) // In a real app, this should only run via HTTPS or capacitor:// where crypto.subtle is available. function fallbackEncrypt(text) { // Simple Base64 + rot13 obfuscation to prevent plain text in localstorage for development const rot13 = str => str.replace(/[a-zA-Z]/g, c => String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26)); return 'FALLBACK:' + btoa(rot13(text)); } function fallbackDecrypt(text) { if (!text.startsWith('FALLBACK:')) return text; // If it's old plain text or we screwed up const rot13 = str => str.replace(/[a-zA-Z]/g, c => String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26)); return rot13(atob(text.replace('FALLBACK:', ''))); } async function getMasterKey() { if (!isCryptoAvailable()) return null; let rawKey = localStorage.getItem('iserv_master_key'); if (!rawKey) { const key = await window.crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); const exported = await window.crypto.subtle.exportKey("raw", key); rawKey = btoa(String.fromCharCode(...new Uint8Array(exported))); localStorage.setItem('iserv_master_key', rawKey); } const keyBytes = Uint8Array.from(atob(rawKey), c => c.charCodeAt(0)); return window.crypto.subtle.importKey( "raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"] ); } export async function encryptPassword(password) { if (!password) return ""; if (!isCryptoAvailable()) { console.warn("Crypto Subtle API not available. Using fallback obfuscation."); return fallbackEncrypt(password); } const key = await getMasterKey(); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(password); const encrypted = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, encoded ); const ivBase64 = btoa(String.fromCharCode(...iv)); const encryptedBase64 = btoa(String.fromCharCode(...new Uint8Array(encrypted))); return `${ivBase64}:${encryptedBase64}`; } export async function decryptPassword(encryptedStr) { if (!encryptedStr) return ""; if (encryptedStr.startsWith('FALLBACK:')) { return fallbackDecrypt(encryptedStr); } if (!isCryptoAvailable()) { console.warn("Crypto Subtle API not available and string is not a fallback. Cannot decrypt."); return ""; } try { const parts = encryptedStr.split(':'); if (parts.length !== 2) return ""; const key = await getMasterKey(); const iv = Uint8Array.from(atob(parts[0]), c => c.charCodeAt(0)); const encryptedData = Uint8Array.from(atob(parts[1]), c => c.charCodeAt(0)); const decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, encryptedData ); return new TextDecoder().decode(decrypted); } catch (e) { console.error("Decryption failed", e); return ""; } }