@@ -8,7 +8,7 @@ const FormData = require('form-data');
88const { autoUpdater } = require ( 'electron-updater' ) ;
99const 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
1414const 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
10791262function sendUploadProgress ( event , requestId , payload ) {
0 commit comments