@benzid.wael/secure-vault 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ import electron from 'electron';
2
+
3
+ const { Menu } = electron;
4
+
5
+ export class MenuService {
6
+ constructor() {
7
+ this.mainWindow = null;
8
+ }
9
+
10
+ createMenu(mainWindow) {
11
+ this.mainWindow = mainWindow;
12
+
13
+ const template = [
14
+ {
15
+ label: 'File',
16
+ submenu: [
17
+ {
18
+ label: 'New Vault',
19
+ accelerator: 'CmdOrCtrl+N',
20
+ click: () => {
21
+ this.sendMenuEvent('menu-new-vault');
22
+ },
23
+ },
24
+ {
25
+ label: 'Open Vault',
26
+ accelerator: 'CmdOrCtrl+O',
27
+ click: () => {
28
+ this.sendMenuEvent('menu-open-vault');
29
+ },
30
+ },
31
+ {
32
+ label: 'Import Vault…',
33
+ accelerator: 'CmdOrCtrl+I',
34
+ click: () => {
35
+ this.sendMenuEvent('menu-import-vault');
36
+ },
37
+ },
38
+ { type: 'separator' },
39
+ {
40
+ label: 'Lock Vault',
41
+ accelerator: 'CmdOrCtrl+L',
42
+ click: () => {
43
+ this.sendMenuEvent('menu-lock-vault');
44
+ },
45
+ },
46
+ { type: 'separator' },
47
+ {
48
+ label: 'Configuration',
49
+ accelerator: 'CmdOrCtrl+S',
50
+ click: () => {
51
+ this.sendMenuEvent('menu-configuration');
52
+ },
53
+ },
54
+ { type: 'separator' },
55
+ { role: 'quit' },
56
+ ],
57
+ },
58
+ {
59
+ label: 'Edit',
60
+ submenu: [
61
+ { role: 'undo' },
62
+ { role: 'redo' },
63
+ { type: 'separator' },
64
+ { role: 'cut' },
65
+ { role: 'copy' },
66
+ { role: 'paste' },
67
+ ],
68
+ },
69
+ {
70
+ label: 'View',
71
+ submenu: [
72
+ { role: 'reload' },
73
+ { role: 'forceReload' },
74
+ { role: 'toggleDevTools' },
75
+ { type: 'separator' },
76
+ { role: 'resetZoom' },
77
+ { role: 'zoomIn' },
78
+ { role: 'zoomOut' },
79
+ { type: 'separator' },
80
+ { role: 'togglefullscreen' },
81
+ ],
82
+ },
83
+ {
84
+ label: 'Window',
85
+ submenu: [{ role: 'minimize' }, { role: 'close' }],
86
+ },
87
+ ];
88
+
89
+ const menu = Menu.buildFromTemplate(template);
90
+ Menu.setApplicationMenu(menu);
91
+ }
92
+
93
+ sendMenuEvent(eventName) {
94
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
95
+ this.mainWindow.webContents.send(eventName);
96
+ }
97
+ }
98
+
99
+ updateMenu(menuItems) {
100
+ // Method to dynamically update menu items
101
+ // This can be used to enable/disable menu items based on application state
102
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
103
+ // Implementation for dynamic menu updates
104
+ }
105
+ }
106
+
107
+ setMainWindow(mainWindow) {
108
+ this.mainWindow = mainWindow;
109
+ }
110
+ }
@@ -0,0 +1,109 @@
1
+ export class SecurityManager {
2
+ constructor() {
3
+ this.securityPolicies = new Map();
4
+ }
5
+
6
+ setupSecurityPolicies() {
7
+ // Define security policies
8
+ this.securityPolicies.set('windowCreation', 'deny');
9
+ this.securityPolicies.set('externalNavigation', 'deny');
10
+ this.securityPolicies.set('remoteContent', 'deny');
11
+ }
12
+
13
+ handleWebContentsCreated(contents) {
14
+ // Prevent new window creation
15
+ contents.on('new-window', (event, navigationUrl) => {
16
+ event.preventDefault();
17
+ });
18
+
19
+ // Additional security measures can be added here
20
+ contents.on('will-navigate', (event, navigationUrl) => {
21
+ // This is handled by WindowManager, but we can add additional checks here
22
+ });
23
+ }
24
+
25
+ validateInput(input, type) {
26
+ switch (type) {
27
+ case 'vaultName':
28
+ return this.validateVaultName(input);
29
+ case 'password':
30
+ return this.validatePassword(input);
31
+ case 'recoveryKey':
32
+ return this.validateRecoveryKey(input);
33
+ default:
34
+ return { isValid: true };
35
+ }
36
+ }
37
+
38
+ validateVaultName(vaultName) {
39
+ if (!vaultName || typeof vaultName !== 'string') {
40
+ return { isValid: false, error: 'Vault name must be a string' };
41
+ }
42
+
43
+ if (vaultName.length < 1 || vaultName.length > 50) {
44
+ return {
45
+ isValid: false,
46
+ error: 'Vault name must be between 1 and 50 characters',
47
+ };
48
+ }
49
+
50
+ // Check for invalid characters
51
+ const invalidChars = /[<>:"/\\|?*]/;
52
+ if (invalidChars.test(vaultName)) {
53
+ return {
54
+ isValid: false,
55
+ error: 'Vault name contains invalid characters',
56
+ };
57
+ }
58
+
59
+ return { isValid: true };
60
+ }
61
+
62
+ validatePassword(password) {
63
+ if (!password || typeof password !== 'string') {
64
+ return { isValid: false, error: 'Password must be a string' };
65
+ }
66
+
67
+ if (password.length < 8) {
68
+ return {
69
+ isValid: false,
70
+ error: 'Password must be at least 8 characters long',
71
+ };
72
+ }
73
+
74
+ return { isValid: true };
75
+ }
76
+
77
+ validateRecoveryKey(recoveryKey) {
78
+ if (!recoveryKey || typeof recoveryKey !== 'string') {
79
+ return { isValid: false, error: 'Recovery key must be a string' };
80
+ }
81
+
82
+ // Remove dashes and convert to uppercase
83
+ const cleanKey = recoveryKey.replace(/-/g, '').toUpperCase();
84
+
85
+ // Check if it matches expected format (base32, specific length)
86
+ const base32Regex = /^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/;
87
+ if (!base32Regex.test(cleanKey) || cleanKey.length < 50) {
88
+ return { isValid: false, error: 'Invalid recovery key format' };
89
+ }
90
+
91
+ return { isValid: true };
92
+ }
93
+
94
+ sanitizePath(filePath) {
95
+ // Basic path sanitization to prevent directory traversal
96
+ return filePath.replace(/\.\./g, '').replace(/\/\//g, '/');
97
+ }
98
+
99
+ isSecureOrigin(url) {
100
+ try {
101
+ const parsedUrl = new URL(url);
102
+ return (
103
+ parsedUrl.protocol === 'file:' || parsedUrl.hostname === 'localhost'
104
+ );
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,137 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export class VaultFileService {
6
+ constructor(vaultDirectory) {
7
+ // Resolve tilde and ensure absolute path
8
+ this.vaultDirectory = this._resolvePath(vaultDirectory);
9
+ this._ensureVaultDirectory();
10
+ }
11
+
12
+ _resolvePath(inputPath) {
13
+ if (inputPath.startsWith('~')) {
14
+ return path.join(os.homedir(), inputPath.slice(1));
15
+ }
16
+ return path.resolve(inputPath);
17
+ }
18
+
19
+ _ensureVaultDirectory() {
20
+ fs.ensureDirSync(this.vaultDirectory);
21
+ }
22
+
23
+ getVaultPath(vaultName) {
24
+ return path.join(this.vaultDirectory, `${vaultName}.vault`);
25
+ }
26
+
27
+ getBackupPath(vaultName) {
28
+ return path.join(this.vaultDirectory, `${vaultName}.vault.backup`);
29
+ }
30
+
31
+ getTempPath(vaultName) {
32
+ return path.join(this.vaultDirectory, `${vaultName}.vault.tmp`);
33
+ }
34
+
35
+ async vaultExists(vaultName) {
36
+ return fs.pathExists(this.getVaultPath(vaultName));
37
+ }
38
+
39
+ async readVaultPath(vaultPath) {
40
+ return fs.readJSON(vaultPath);
41
+ }
42
+
43
+ async writeVaultPath(vaultPath, data) {
44
+ return fs.writeJSON(vaultPath, data, { spaces: 2 });
45
+ }
46
+
47
+ async readVaultFile(vaultName) {
48
+ const vaultPath = this.getVaultPath(vaultName);
49
+ return this.readVaultPath(vaultPath);
50
+ }
51
+
52
+ async writeVaultFile(vaultName, data) {
53
+ const vaultPath = this.getVaultPath(vaultName);
54
+ return fs.writeJSON(vaultPath, data, { spaces: 2 });
55
+ }
56
+
57
+ async atomicWriteVaultFile(vaultName, data) {
58
+ const vaultPath = this.getVaultPath(vaultName);
59
+ const tempPath = this.getTempPath(vaultName);
60
+
61
+ // Write to temp file first
62
+ await fs.writeJSON(tempPath, data, { spaces: 2 });
63
+
64
+ // Verify temp file can be read
65
+ await fs.readJSON(tempPath);
66
+
67
+ // Move temp file to final location (atomic operation)
68
+ await fs.move(tempPath, vaultPath, { overwrite: true });
69
+ }
70
+
71
+ async createBackup(vaultName) {
72
+ const vaultPath = this.getVaultPath(vaultName);
73
+ const backupPath = this.getBackupPath(vaultName);
74
+
75
+ if (await fs.pathExists(vaultPath)) {
76
+ await fs.copy(vaultPath, backupPath);
77
+ }
78
+ }
79
+
80
+ async restoreFromBackup(vaultName) {
81
+ const vaultPath = this.getVaultPath(vaultName);
82
+ const backupPath = this.getBackupPath(vaultName);
83
+
84
+ if (await fs.pathExists(backupPath)) {
85
+ await fs.copy(backupPath, vaultPath);
86
+ await fs.remove(backupPath);
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ async hasBackup(vaultName) {
93
+ return fs.pathExists(this.getBackupPath(vaultName));
94
+ }
95
+
96
+ async deleteVault(vaultName) {
97
+ const vaultPath = this.getVaultPath(vaultName);
98
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
99
+ const deletedBackupPath = path.join(
100
+ this.vaultDirectory,
101
+ `${vaultName}.vault.deleted.${timestamp}`
102
+ );
103
+
104
+ // Create backup before deletion
105
+ await fs.copy(vaultPath, deletedBackupPath);
106
+
107
+ // Delete main vault file
108
+ await fs.remove(vaultPath);
109
+
110
+ // Clean up related files
111
+ const relatedFiles = [
112
+ this.getBackupPath(vaultName),
113
+ this.getTempPath(vaultName),
114
+ path.join(this.vaultDirectory, `${vaultName}.recovery`),
115
+ ];
116
+
117
+ for (const filePath of relatedFiles) {
118
+ if (await fs.pathExists(filePath)) {
119
+ await fs.remove(filePath);
120
+ }
121
+ }
122
+
123
+ return path.basename(deletedBackupPath);
124
+ }
125
+
126
+ async listVaults() {
127
+ try {
128
+ const files = await fs.readdir(this.vaultDirectory);
129
+ return files
130
+ .filter((file) => file.endsWith('.vault'))
131
+ .map((file) => file.replace('.vault', ''));
132
+ } catch (error) {
133
+ console.error('Error listing vaults:', error);
134
+ return [];
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,134 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import chalk from 'chalk';
5
+
6
+ import { VaultFileService } from './VaultFileService.js';
7
+ import { VaultService } from './VaultService.js';
8
+ import { PasswordRecoveryService } from './recovery/PasswordRecoveryService.js';
9
+ import { KeyRecoveryService } from './recovery/KeyRecoveryService.js';
10
+ import { CryptographyService } from './CryptographyService.js';
11
+ import { Vault } from '../models/Vault.js';
12
+
13
+ export class VaultRecoveryService {
14
+ constructor(vaultDir) {
15
+ this.vaultDir = vaultDir;
16
+ }
17
+
18
+ async recover(vaultName, masterPassword) {
19
+ const vfs = new VaultFileService(this.vaultDir);
20
+ if (!vfs.vaultExists(vaultName)) {
21
+ console.error(chalk.red('Vault not found!'));
22
+ return false;
23
+ }
24
+
25
+ const vs = new VaultService(this.vaultDir);
26
+ const is_valid = await vs.verifyPassword(vaultName, masterPassword);
27
+ if (!is_valid.success) {
28
+ console.error(chalk.red(is_valid.error));
29
+ return false;
30
+ }
31
+
32
+ const vaultFile = await vfs.readVaultFile(vaultName);
33
+
34
+ const salt = Buffer.from(vaultFile.salt, 'hex');
35
+ const key = CryptographyService.deriveKey(masterPassword, salt);
36
+ // checking data
37
+ const encryptedData = {
38
+ encrypted: vaultFile.encrypted,
39
+ authTag: vaultFile.authTag,
40
+ iv: vaultFile.iv,
41
+ };
42
+
43
+ let vaultData = CryptographyService.decrypt(encryptedData, key);
44
+ let vault = Vault.fromJSON(vaultData, vaultName);
45
+ vaultData = null;
46
+ let serializedVault = vault.toJSON();
47
+ vault = null;
48
+ serializedVault = null;
49
+
50
+ console.log(vaultFile);
51
+ const existingMetadata = vaultFile.recoveryMetadata || {};
52
+ let recoveryMetadataFlatten = [];
53
+
54
+ await Promise.all(
55
+ [
56
+ new PasswordRecoveryService(this.vaultDir),
57
+ new KeyRecoveryService(this.vaultDir),
58
+ ].flatMap(async (recovery) => {
59
+ const methodId = recovery.getRecoveryMethodId();
60
+ const metadata = existingMetadata[methodId] || {};
61
+ if (!recovery.isValid(vaultName, metadata)) {
62
+ console.error(
63
+ chalk.red('❤️‍🩹 Invalid recovery data found, method: ', methodId)
64
+ );
65
+ console.log('🛟 Fixing recovery data');
66
+
67
+ const recoveryData = await recovery.generate();
68
+ const newMetadata = recovery.createMetadata(
69
+ vaultName,
70
+ masterPassword,
71
+ recoveryData
72
+ );
73
+ console.log(newMetadata);
74
+ recoveryMetadataFlatten.push({
75
+ name: methodId,
76
+ metadata: newMetadata,
77
+ });
78
+ return;
79
+ }
80
+
81
+ chalk.green('✅ Valid recovery data for method: ', methodId);
82
+ recoveryMetadataFlatten.push({
83
+ name: methodId,
84
+ metadata: metadata,
85
+ });
86
+ })
87
+ );
88
+
89
+ console.log(recoveryMetadataFlatten);
90
+ const recoveryMetadata = recoveryMetadataFlatten.reduce((obj, item) => {
91
+ obj[item.name] = item.metadata;
92
+ return obj;
93
+ }, {});
94
+
95
+ console.log(chalk.green('✅ Successfully recovered vault!'));
96
+ const newVaultData = {
97
+ ...encryptedData,
98
+ salt: vaultFile.salt,
99
+ recoveryMetadata,
100
+ };
101
+
102
+ // create in temp file
103
+ // Atomically write the new vault file
104
+ const tempPath = vfs.getTempPath(vaultName);
105
+ await vfs.writeVaultPath(tempPath, newVaultData);
106
+ // check
107
+ const is_temp_vault_valid = await vs.verifyPassword(
108
+ vaultName,
109
+ masterPassword,
110
+ tempPath
111
+ );
112
+ console.log(is_temp_vault_valid);
113
+ if (!is_temp_vault_valid.success) {
114
+ console.error(chalk.red(is_valid.error));
115
+ return false;
116
+ }
117
+
118
+ console.log(chalk.green('✅ Temporary vault is working as expected!'));
119
+ let idx = 0;
120
+ let fileName = `${vaultName}-recovered.vault`;
121
+ while (await fs.pathExists(path.join(this.vaultDir, finalName))) {
122
+ idx++;
123
+ fileName = `${vaultName}-recovered-${idx}.vault`;
124
+ }
125
+ // Move temp file to final location (atomic operation)
126
+ await fs.move(tempPath, path.join(this.vaultDir, finalName), {
127
+ overwrite: true,
128
+ });
129
+ console.log(
130
+ chalk.green('✅ Vault recovered successfully, new Vault: ', fileName)
131
+ );
132
+ return true;
133
+ }
134
+ }