172 lines
6.5 KiB
JavaScript
172 lines
6.5 KiB
JavaScript
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 startUrl = process.env.ELECTRON_START_URL || `file://${path.join(__dirname, '../dist/index.html')}`;
|
|
|
|
if (process.env.npm_lifecycle_event === 'electron:dev') {
|
|
win.loadURL('http://localhost:5173');
|
|
win.webContents.openDevTools();
|
|
} else {
|
|
win.loadURL(startUrl);
|
|
}
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
createWindow();
|
|
|
|
// Helper to fetch metadata (Zero-Knowledge: Client fetches previews)
|
|
ipcMain.handle('fetch-metadata', async (event, url) => {
|
|
return new Promise((resolve) => {
|
|
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(`<meta\\s+(?:name|property)=["'](?:og:)?${prop}["']\\s+content=["'](.*?)["']`, 'i');
|
|
const match = data.match(regex);
|
|
return match ? match[1] : null;
|
|
};
|
|
const getTitle = () => {
|
|
const regex = /<title>(.*?)<\/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);
|
|
});
|
|
|
|
// 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('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) => {
|
|
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);
|
|
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
return decrypted;
|
|
});
|
|
});
|