Skip to content

Commit 018cba8

Browse files
author
FileShot
committed
v1.4.2: Fully functional Local Vault with encryption
- Fix logo: Use FileShot folder+lightning icon instead of download SVG - Fix tools dropdown: Add CSS rule for [hidden] attribute to work with flex display - Local Vault now encrypts files with AES-256-GCM on add - Add Open button to decrypt and preview vault files - Add Export button to decrypt and save vault files - Add Export Vault Backup (JSON with all encrypted files) - Add Import Vault Backup functionality - Update vault item display with encryption indicator - Add confirmation dialog before removing vault items - Update Explorer hint text - Version bump to 1.4.2
1 parent 336ac45 commit 018cba8

6 files changed

Lines changed: 422 additions & 26 deletions

File tree

main.js

Lines changed: 194 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const FormData = require('form-data');
88
const { autoUpdater } = require('electron-updater');
99
const Store = require('electron-store');
1010

11-
const { encryptFileToZkeContainer } = require('./utils/zke-stream');
11+
const { encryptFileToZkeContainer, decryptZkeContainer, parseHeader } = require('./utils/zke-stream');
1212

1313
// Initialize electron-store for settings
1414
const store = new Store();
@@ -67,26 +67,40 @@ function safeStat(p) {
6767
try { return fs.statSync(p); } catch (_) { return null; }
6868
}
6969

70-
function addFileToVault(sourcePath) {
70+
/**
71+
* Add a file to the vault with AES-256-GCM encryption.
72+
* The file is encrypted using a randomly generated key stored in the vault metadata.
73+
*/
74+
async function addFileToVault(sourcePath) {
7175
ensureVaultDirs();
7276
const st = safeStat(sourcePath);
7377
if (!st || !st.isFile()) return null;
7478

7579
const id = genId();
7680
const name = path.basename(sourcePath);
77-
const ext = path.extname(name);
78-
const storedName = `${id}${ext || ''}`;
81+
const storedName = `${id}.fszk`; // Always use .fszk extension for encrypted files
7982
const destPath = path.join(getVaultFilesDir(), storedName);
8083

81-
fs.copyFileSync(sourcePath, destPath);
84+
// Encrypt the file using ZKE format
85+
const result = await encryptFileToZkeContainer({
86+
inputPath: sourcePath,
87+
outputPath: destPath,
88+
originalName: name,
89+
mode: 'raw' // Use random key (stored in vault metadata)
90+
});
91+
92+
const encryptedStat = safeStat(destPath);
8293

8394
return {
8495
id,
8596
name,
86-
size: st.size,
97+
originalSize: st.size,
98+
size: encryptedStat ? encryptedStat.size : st.size,
8799
addedAt: Date.now(),
88100
localPath: destPath,
89-
sourcePath
101+
sourcePath,
102+
encrypted: true,
103+
encryptionKey: result.rawKey // Base64url encoded AES-256 key
90104
};
91105
}
92106

@@ -1007,7 +1021,7 @@ ipcMain.handle('vault-add', async (_event, paths) => {
10071021

10081022
for (const p of list) {
10091023
try {
1010-
const entry = addFileToVault(p);
1024+
const entry = await addFileToVault(p);
10111025
if (entry) {
10121026
items.unshift(entry);
10131027
added++;
@@ -1032,7 +1046,7 @@ ipcMain.handle('vault-add-folder', async (_event, folderPath) => {
10321046

10331047
for (const p of files) {
10341048
try {
1035-
const entry = addFileToVault(p);
1049+
const entry = await addFileToVault(p);
10361050
if (entry) {
10371051
items.unshift(entry);
10381052
added++;
@@ -1072,8 +1086,177 @@ ipcMain.handle('vault-reveal-key', async (_event, localId) => {
10721086
const items = getVaultItems();
10731087
const it = items.find((x) => String(x.id) === id);
10741088
if (!it) return { success: false };
1075-
// Only reveal a stored share key (raw-key mode). Passphrase mode has no share key.
1076-
return { success: true, shareKey: it?.lastUpload?.shareKey || null, shareUrl: it?.lastUpload?.shareUrl || null };
1089+
// Return the encryption key for this vault item
1090+
return {
1091+
success: true,
1092+
encryptionKey: it.encryptionKey || null,
1093+
shareKey: it?.lastUpload?.shareKey || null,
1094+
shareUrl: it?.lastUpload?.shareUrl || null
1095+
};
1096+
});
1097+
1098+
// Open/preview a vault file by decrypting to temp and opening
1099+
ipcMain.handle('vault-open', async (_event, localId) => {
1100+
const id = String(localId || '');
1101+
const items = getVaultItems();
1102+
const it = items.find((x) => String(x.id) === id);
1103+
if (!it) return { success: false, error: 'Not found' };
1104+
if (!it.localPath || !fs.existsSync(it.localPath)) return { success: false, error: 'File missing' };
1105+
1106+
// For encrypted files, decrypt to temp folder
1107+
if (it.encrypted && it.encryptionKey) {
1108+
try {
1109+
ensureVaultDirs();
1110+
const tmpPath = path.join(getVaultTmpDir(), it.name);
1111+
await decryptZkeContainer({
1112+
inputPath: it.localPath,
1113+
outputPath: tmpPath,
1114+
rawKeyBase64Url: it.encryptionKey
1115+
});
1116+
await shell.openPath(tmpPath);
1117+
return { success: true, tmpPath };
1118+
} catch (e) {
1119+
return { success: false, error: e.message || String(e) };
1120+
}
1121+
}
1122+
1123+
// For legacy non-encrypted files, open directly
1124+
try {
1125+
await shell.openPath(it.localPath);
1126+
return { success: true };
1127+
} catch (e) {
1128+
return { success: false, error: e.message || String(e) };
1129+
}
1130+
});
1131+
1132+
// Export a single vault file (decrypt and save to user-chosen location)
1133+
ipcMain.handle('vault-export-file', async (_event, localId) => {
1134+
const id = String(localId || '');
1135+
const items = getVaultItems();
1136+
const it = items.find((x) => String(x.id) === id);
1137+
if (!it) return { success: false, error: 'Not found' };
1138+
if (!it.localPath || !fs.existsSync(it.localPath)) return { success: false, error: 'File missing' };
1139+
1140+
const result = await dialog.showSaveDialog(mainWindow, {
1141+
defaultPath: it.name,
1142+
title: 'Export Decrypted File'
1143+
});
1144+
1145+
if (result.canceled || !result.filePath) return { success: false, canceled: true };
1146+
1147+
if (it.encrypted && it.encryptionKey) {
1148+
try {
1149+
await decryptZkeContainer({
1150+
inputPath: it.localPath,
1151+
outputPath: result.filePath,
1152+
rawKeyBase64Url: it.encryptionKey
1153+
});
1154+
return { success: true, path: result.filePath };
1155+
} catch (e) {
1156+
return { success: false, error: e.message || String(e) };
1157+
}
1158+
}
1159+
1160+
// Legacy non-encrypted: just copy
1161+
try {
1162+
fs.copyFileSync(it.localPath, result.filePath);
1163+
return { success: true, path: result.filePath };
1164+
} catch (e) {
1165+
return { success: false, error: e.message || String(e) };
1166+
}
1167+
});
1168+
1169+
// Export entire vault as encrypted backup archive
1170+
ipcMain.handle('vault-export-all', async () => {
1171+
const items = getVaultItems();
1172+
if (!items.length) return { success: false, error: 'Vault is empty' };
1173+
1174+
const result = await dialog.showSaveDialog(mainWindow, {
1175+
defaultPath: `fileshot-vault-backup-${Date.now()}.json`,
1176+
title: 'Export Vault Backup',
1177+
filters: [{ name: 'FileShot Vault Backup', extensions: ['json'] }]
1178+
});
1179+
1180+
if (result.canceled || !result.filePath) return { success: false, canceled: true };
1181+
1182+
try {
1183+
// Create backup object with metadata and base64-encoded encrypted files
1184+
const backup = {
1185+
version: 1,
1186+
exportedAt: Date.now(),
1187+
files: []
1188+
};
1189+
1190+
for (const it of items) {
1191+
if (!it.localPath || !fs.existsSync(it.localPath)) continue;
1192+
const fileData = fs.readFileSync(it.localPath);
1193+
backup.files.push({
1194+
id: it.id,
1195+
name: it.name,
1196+
originalSize: it.originalSize || it.size,
1197+
addedAt: it.addedAt,
1198+
encrypted: it.encrypted || false,
1199+
encryptionKey: it.encryptionKey || null,
1200+
data: fileData.toString('base64')
1201+
});
1202+
}
1203+
1204+
fs.writeFileSync(result.filePath, JSON.stringify(backup, null, 2));
1205+
return { success: true, path: result.filePath, count: backup.files.length };
1206+
} catch (e) {
1207+
return { success: false, error: e.message || String(e) };
1208+
}
1209+
});
1210+
1211+
// Import vault from backup
1212+
ipcMain.handle('vault-import', async () => {
1213+
const result = await dialog.showOpenDialog(mainWindow, {
1214+
title: 'Import Vault Backup',
1215+
filters: [{ name: 'FileShot Vault Backup', extensions: ['json'] }],
1216+
properties: ['openFile']
1217+
});
1218+
1219+
if (result.canceled || !result.filePaths.length) return { success: false, canceled: true };
1220+
1221+
try {
1222+
const content = fs.readFileSync(result.filePaths[0], 'utf8');
1223+
const backup = JSON.parse(content);
1224+
1225+
if (!backup.files || !Array.isArray(backup.files)) {
1226+
return { success: false, error: 'Invalid backup format' };
1227+
}
1228+
1229+
ensureVaultDirs();
1230+
const items = getVaultItems();
1231+
let imported = 0;
1232+
1233+
for (const file of backup.files) {
1234+
const id = genId();
1235+
const storedName = `${id}.fszk`;
1236+
const destPath = path.join(getVaultFilesDir(), storedName);
1237+
1238+
const fileData = Buffer.from(file.data, 'base64');
1239+
fs.writeFileSync(destPath, fileData);
1240+
1241+
items.unshift({
1242+
id,
1243+
name: file.name,
1244+
originalSize: file.originalSize,
1245+
size: fileData.length,
1246+
addedAt: file.addedAt || Date.now(),
1247+
localPath: destPath,
1248+
encrypted: file.encrypted || false,
1249+
encryptionKey: file.encryptionKey || null,
1250+
importedFrom: result.filePaths[0]
1251+
});
1252+
imported++;
1253+
}
1254+
1255+
setVaultItems(items);
1256+
return { success: true, imported };
1257+
} catch (e) {
1258+
return { success: false, error: e.message || String(e) };
1259+
}
10771260
});
10781261

10791262
function sendUploadProgress(event, requestId, payload) {

preload.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
3434
vaultAddFolder: (folderPath) => ipcRenderer.invoke('vault-add-folder', folderPath),
3535
vaultRemove: (localId) => ipcRenderer.invoke('vault-remove', localId),
3636
vaultRevealKey: (localId) => ipcRenderer.invoke('vault-reveal-key', localId),
37+
vaultOpen: (localId) => ipcRenderer.invoke('vault-open', localId),
38+
vaultExportFile: (localId) => ipcRenderer.invoke('vault-export-file', localId),
39+
vaultExportAll: () => ipcRenderer.invoke('vault-export-all'),
40+
vaultImport: () => ipcRenderer.invoke('vault-import'),
3741

3842
// ZKE upload
3943
uploadZke: (localId, options, progressCb) => {

0 commit comments

Comments
 (0)