const { app, BrowserWindow, ipcMain, shell } = require('electron'); const path = require('path'); const https = require('https'); const http = require('http'); function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.cjs'), sandbox: false } }); const isDev = process.env.npm_lifecycle_event === 'electron:dev'; if (isDev) { win.loadURL('http://localhost:5173'); win.webContents.openDevTools(); } else { // Production: Load the built file // dist-react is in the same directory as main.cjs win.loadFile(path.join(__dirname, 'dist-react', 'index.html')); } } app.whenReady().then(() => { createWindow(); // Helper to fetch metadata (Zero-Knowledge: Client fetches previews) ipcMain.handle('fetch-metadata', async (event, url) => { return new Promise((resolve) => { // Check for direct video links to avoid downloading large files const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov']; const imageExtensions = ['.gif', '.png', '.jpg', '.jpeg', '.webp']; const lowerUrl = url.toLowerCase(); if (videoExtensions.some(ext => lowerUrl.endsWith(ext))) { const filename = url.split('/').pop(); resolve({ title: filename, siteName: new URL(url).hostname, video: url, image: null, // No thumbnail for now unless we generate one description: 'Video File' }); return; } if (imageExtensions.some(ext => lowerUrl.endsWith(ext))) { const filename = url.split('/').pop(); resolve({ title: filename, siteName: new URL(url).hostname, video: null, image: url, // Direct image/gif description: 'Image File' }); return; } const client = url.startsWith('https') ? https : http; const req = client.get(url, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { // Simple Regex Parser for OG Tags to avoid dependencies const getMeta = (prop) => { const regex = new RegExp(` { const regex = /(.*?)<\/title>/i; const match = data.match(regex); return match ? match[1] : null; }; const metadata = { title: getMeta('title') || getTitle(), description: getMeta('description'), image: getMeta('image'), siteName: getMeta('site_name'), themeColor: getMeta('theme-color') }; resolve(metadata); }); }); req.on('error', (err) => { console.error('Metadata fetch error:', err); resolve(null); }); }); }); ipcMain.handle('open-external', async (event, url) => { await shell.openExternal(url); }); ipcMain.handle('get-screen-sources', async () => { const { desktopCapturer } = require('electron'); const sources = await desktopCapturer.getSources({ types: ['window', 'screen'], thumbnailSize: { width: 450, height: 250, borderRadius: '8px' }, fetchWindowIcons: true }); return sources.map(source => ({ id: source.id, name: source.name, thumbnail: source.thumbnail.toDataURL(), appIcon: source.appIcon ? source.appIcon.toDataURL() : null })); }); // Crypto Handlers const crypto = require('crypto'); ipcMain.handle('generate-keys', async () => { const generateRSA = () => new Promise((resolve, reject) => { crypto.generateKeyPair('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }, (err, pub, priv) => { /* args are (err, publicKey, privateKey) */ if (err) reject(err); else resolve({ pub, priv }); }); }); const generateEd = () => new Promise((resolve, reject) => { crypto.generateKeyPair('ed25519', { publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }, (err, pub, priv) => { if (err) reject(err); else resolve({ pub, priv }); }); }); const [rsa, ed] = await Promise.all([generateRSA(), generateEd()]); return { rsaPub: rsa.pub, rsaPriv: rsa.priv, edPub: ed.pub, edPriv: ed.priv }; }); ipcMain.handle('random-bytes', (event, size) => { return crypto.randomBytes(size).toString('hex'); }); ipcMain.handle('sha256', (event, data) => { return crypto.createHash('sha256').update(data).digest('hex'); }); ipcMain.handle('sign-message', (event, privateKeyPem, message) => { // message should be a string or buffer return crypto.sign(null, Buffer.from(message), privateKeyPem).toString('hex'); }); ipcMain.handle('verify-signature', (event, publicKeyPem, message, signature) => { return crypto.verify(null, Buffer.from(message), publicKeyPem, Buffer.from(signature, 'hex')); }); ipcMain.handle('derive-auth-keys', (event, password, salt) => { return new Promise((resolve, reject) => { crypto.scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); else { const dak = derivedKey.subarray(0, 32).toString('hex'); const dek = derivedKey.subarray(32, 64); resolve({ dak, dek }); } }); }); }); ipcMain.handle('public-encrypt', (event, publicKeyPem, data) => { // data can be string or buffer. Returns hex. const buffer = Buffer.from(data); return crypto.publicEncrypt({ key: publicKeyPem, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256", }, buffer).toString('hex'); }); ipcMain.handle('private-decrypt', (event, privateKeyPem, encryptedHex) => { const buffer = Buffer.from(encryptedHex, 'hex'); return crypto.privateDecrypt({ key: privateKeyPem, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256", }, buffer).toString(); // Assuming utf8 string output }); ipcMain.handle('encrypt-data', (event, plaintext, key) => { console.log('encrypt-data called with:', { plaintextType: typeof plaintext, isBuffer: Buffer.isBuffer(plaintext), keyType: typeof key, keyIsBuffer: Buffer.isBuffer(plaintext) }); if (plaintext === undefined) throw new TypeError('plaintext is undefined'); if (key === undefined) throw new TypeError('key is undefined'); // Key can be hex string or buffer. 32 bytes for AES-256 const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key; const iv = crypto.randomBytes(12); // 96-bit IV for GCM const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv); // Use Buffer concatenation to handle both string and Buffer inputs correctly without encoding confusion const encryptedBuffer = Buffer.concat([ cipher.update(plaintext), cipher.final() ]); const tag = cipher.getAuthTag().toString('hex'); return { content: encryptedBuffer.toString('hex'), iv: iv.toString('hex'), tag }; }); ipcMain.handle('decrypt-data', (event, ciphertext, key, iv, tag, options = {}) => { const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key; const ivBuffer = Buffer.from(iv, 'hex'); const tagBuffer = Buffer.from(tag, 'hex'); const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer); decipher.setAuthTag(tagBuffer); const outputEncoding = options.encoding || 'utf8'; // If encoding is 'buffer', don't pass an encoding to update/final const updateEncoding = outputEncoding === 'buffer' ? undefined : outputEncoding; let decrypted; if (outputEncoding === 'buffer') { decrypted = Buffer.concat([ decipher.update(ciphertext, 'hex'), decipher.final() ]); } else { decrypted = decipher.update(ciphertext, 'hex', outputEncoding); decrypted += decipher.final(outputEncoding); } return decrypted; }); });