@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,266 @@
1
+ import { BrowserWindow, dialog } from 'electron';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import fs from 'fs';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export class WindowManager {
10
+ constructor() {
11
+ this.mainWindow = null;
12
+ // Check if we're in development mode at runtime
13
+ // Use ELECTRON_IS_DEV or NODE_ENV (ELECTRON_IS_DEV takes precedence)
14
+ this.isDev =
15
+ process.env.ELECTRON_IS_DEV === '1' ||
16
+ process.env.NODE_ENV === 'development';
17
+ console.log(
18
+ 'WindowManager initialized with ELECTRON_IS_DEV:',
19
+ process.env.ELECTRON_IS_DEV,
20
+ 'NODE_ENV:',
21
+ process.env.NODE_ENV,
22
+ 'isDev:',
23
+ this.isDev
24
+ );
25
+ }
26
+
27
+ createMainWindow() {
28
+ try {
29
+ console.log('Creating main browser window...');
30
+
31
+ // Determine the correct preload path based on whether we're bundled or not
32
+ // When bundled, preload.cjs is in the same directory as main.cjs (build/electron/)
33
+ // When in development source, it's in public/
34
+ let preloadPath;
35
+ const bundledPreloadPath = path.join(__dirname, 'preload.cjs');
36
+ const devPreloadPath = path.join(__dirname, '../../../public/preload.js');
37
+
38
+ if (fs.existsSync(bundledPreloadPath)) {
39
+ preloadPath = bundledPreloadPath;
40
+ } else if (fs.existsSync(devPreloadPath)) {
41
+ preloadPath = devPreloadPath;
42
+ } else {
43
+ throw new Error(
44
+ `Preload script not found. Tried: ${bundledPreloadPath}, ${devPreloadPath}`
45
+ );
46
+ }
47
+
48
+ // Check if icon exists (try multiple locations)
49
+ let iconPath;
50
+ const bundledIconPath = path.join(__dirname, '../icon.png');
51
+ const devIconPath = path.join(__dirname, '../../../public/icon.png');
52
+
53
+ if (fs.existsSync(bundledIconPath)) {
54
+ iconPath = bundledIconPath;
55
+ } else if (fs.existsSync(devIconPath)) {
56
+ iconPath = devIconPath;
57
+ } else {
58
+ console.warn(
59
+ `Icon not found. Tried: ${bundledIconPath}, ${devIconPath}`
60
+ );
61
+ }
62
+
63
+ // Create the browser window with security settings
64
+ this.mainWindow = new BrowserWindow({
65
+ width: 1200,
66
+ height: 800,
67
+ minWidth: 800,
68
+ minHeight: 600,
69
+ webPreferences: {
70
+ nodeIntegration: false,
71
+ contextIsolation: true,
72
+ enableRemoteModule: false,
73
+ preload: preloadPath,
74
+ webSecurity: true,
75
+ allowRunningInsecureContent: false,
76
+ experimentalFeatures: false,
77
+ devTools: this.isDev,
78
+ webviewTag: false,
79
+ },
80
+ icon: fs.existsSync(iconPath) ? iconPath : undefined,
81
+ show: false,
82
+ titleBarStyle: 'default',
83
+ });
84
+
85
+ // Handle window events
86
+ this.setupWindowEvents();
87
+
88
+ // Set up security handlers
89
+ this.setupSecurityHandlers();
90
+
91
+ // Load the app
92
+ this.loadApp();
93
+
94
+ return this.mainWindow;
95
+ } catch (error) {
96
+ console.error('Failed to create main window:', error);
97
+ dialog.showErrorBox(
98
+ 'Window Creation Error',
99
+ `Failed to create main window: ${error.message}`
100
+ );
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ async loadApp() {
106
+ try {
107
+ let startUrl;
108
+
109
+ if (this.isDev) {
110
+ startUrl = 'http://localhost:3000';
111
+ console.log(
112
+ 'Development mode: Loading from development server at',
113
+ startUrl
114
+ );
115
+ } else {
116
+ // When bundled, the build is in the parent directory
117
+ const indexPath = path.join(__dirname, '../index.html');
118
+ if (!fs.existsSync(indexPath)) {
119
+ throw new Error(
120
+ `Production build not found at: ${indexPath}. Please run 'npm run build' first.`
121
+ );
122
+ }
123
+ startUrl = `file://${indexPath}`;
124
+ console.log(
125
+ 'Production mode: Loading from build directory at',
126
+ startUrl
127
+ );
128
+ }
129
+
130
+ console.log('Loading URL:', startUrl);
131
+
132
+ // Set up error handling for the window
133
+ this.mainWindow.webContents.on(
134
+ 'did-fail-load',
135
+ (event, errorCode, errorDescription) => {
136
+ console.error('Failed to load URL:', {
137
+ startUrl,
138
+ errorCode,
139
+ errorDescription,
140
+ });
141
+ this.showErrorPage(`Failed to load application: ${errorDescription}`);
142
+ }
143
+ );
144
+
145
+ // Load the URL
146
+ await this.mainWindow.loadURL(startUrl);
147
+
148
+ // Show the window once content is loaded
149
+ this.mainWindow.once('ready-to-show', () => {
150
+ console.log('Window is ready to show');
151
+ this.mainWindow.show();
152
+
153
+ // Focus on the window
154
+ if (this.mainWindow.isMinimized()) {
155
+ this.mainWindow.restore();
156
+ }
157
+ this.mainWindow.focus();
158
+ });
159
+ } catch (error) {
160
+ console.error('Error in loadApp:', error);
161
+ this.showErrorPage(`Failed to load application: ${error.message}`);
162
+ }
163
+ }
164
+
165
+ showErrorPage(errorMessage) {
166
+ const errorHtml = `
167
+ <!DOCTYPE html>
168
+ <html>
169
+ <head>
170
+ <title>Application Error</title>
171
+ <style>
172
+ body {
173
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
174
+ display: flex;
175
+ justify-content: center;
176
+ align-items: center;
177
+ height: 100vh;
178
+ margin: 0;
179
+ background-color: #f8f9fa;
180
+ color: #212529;
181
+ }
182
+ .error-container {
183
+ max-width: 600px;
184
+ padding: 2rem;
185
+ background: white;
186
+ border-radius: 8px;
187
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
188
+ text-align: center;
189
+ }
190
+ h1 { color: #dc3545; margin-top: 0; }
191
+ pre {
192
+ background: #f8f9fa;
193
+ padding: 1rem;
194
+ border-radius: 4px;
195
+ text-align: left;
196
+ max-height: 200px;
197
+ overflow-y: auto;
198
+ }
199
+ </style>
200
+ </head>
201
+ <body>
202
+ <div class="error-container">
203
+ <h1>Application Error</h1>
204
+ <p>An error occurred while starting the application:</p>
205
+ <pre>${errorMessage}</pre>
206
+ <p>Please check the console for more details and try again.</p>
207
+ </div>
208
+ </body>
209
+ </html>
210
+ `;
211
+
212
+ this.mainWindow.loadURL(
213
+ `data:text/html;charset=utf-8,${encodeURIComponent(errorHtml)}`
214
+ );
215
+ }
216
+
217
+ setupWindowEvents() {
218
+ // Show window when ready to prevent visual flash
219
+ this.mainWindow.once('ready-to-show', () => {
220
+ this.mainWindow.show();
221
+ });
222
+
223
+ // Open DevTools in development
224
+ if (this.isDev) {
225
+ this.mainWindow.webContents.openDevTools();
226
+ }
227
+
228
+ // Handle window closed
229
+ this.mainWindow.on('closed', () => {
230
+ this.mainWindow = null;
231
+ });
232
+ }
233
+
234
+ setupSecurityHandlers() {
235
+ // Security: Prevent new window creation
236
+ this.mainWindow.webContents.setWindowOpenHandler(() => {
237
+ return { action: 'deny' };
238
+ });
239
+
240
+ // Security: Prevent navigation to external URLs
241
+ this.mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
242
+ if (this.isDev) {
243
+ const parsedUrl = new URL(navigationUrl);
244
+ if (parsedUrl.origin !== 'http://localhost:3000') {
245
+ event.preventDefault();
246
+ }
247
+ } else {
248
+ // In production, only allow file:// protocol
249
+ const parsedUrl = new URL(navigationUrl);
250
+ if (parsedUrl.protocol !== 'file:') {
251
+ event.preventDefault();
252
+ }
253
+ }
254
+ });
255
+ }
256
+
257
+ getMainWindow() {
258
+ return this.mainWindow;
259
+ }
260
+
261
+ sendToRenderer(channel, data) {
262
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
263
+ this.mainWindow.webContents.send(channel, data);
264
+ }
265
+ }
266
+ }
@@ -0,0 +1,88 @@
1
+ export class RecoveryData {
2
+ constructor({ data, isEncrypted = false, hint = '' } = {}) {
3
+ this.data = data;
4
+ this.isEncrypted = isEncrypted;
5
+ this.hint = hint;
6
+ }
7
+
8
+ toJSON() {
9
+ return {
10
+ data: this.data,
11
+ isEncrypted: this.isEncrypted,
12
+ hint: this.hint,
13
+ };
14
+ }
15
+ }
16
+
17
+ export class IRecoveryMethod {
18
+ /**
19
+ * Get the unique identifier for this recovery method
20
+ * @returns {string} Unique identifier
21
+ */
22
+ getRecoveryMethodId() {
23
+ throw new Error('getRecoveryMethodId() must be implemented by subclass');
24
+ }
25
+
26
+ /**
27
+ * Check if the metadata is valid
28
+ * @param {string} vaultName
29
+ * @param {Object} metadata
30
+ * @returns {Promise<boolean>}
31
+ */
32
+ isValid(vaultName, metadata) {
33
+ throw new Error('isValid() must be implemented by subclass');
34
+ }
35
+
36
+ /**
37
+ * Generate new recovery method
38
+ * @param {string} vaultName
39
+ * @returns {Promise<RecoveryData>} RecoveryData
40
+ */
41
+ async generate(vaultName) {
42
+ throw new Error('initialize() must be implemented by subclass');
43
+ }
44
+
45
+ /**
46
+ * Create and returns metadata for the recovery option
47
+ * @param {string} vaultName
48
+ * @param {str} masterPassword
49
+ * @param {Object} recoveryData
50
+ * @returns {Promise<Object>} - metadata object
51
+ */
52
+ createMetadata(vaultName, masterPassword, recoveryData = {}) {
53
+ throw new Error('createMetadata() must be implemented by subclass');
54
+ }
55
+
56
+ /**
57
+ * Verify recovery data
58
+ * @param {string} vaultName
59
+ * @param {Object} metadata
60
+ * @param {Object} recoveryData
61
+ * @returns {Promise<boolean>} - Boolean indicating whether key is valid or not!
62
+ */
63
+ async verify(vaultName, metadata = {}, recoveryData = {}) {
64
+ throw new Error('verify() must be implemented by subclass');
65
+ }
66
+
67
+ /**
68
+ * recover master password
69
+ * @param {string} vaultName
70
+ * @param {Object} recoveryData
71
+ * @returns {Promise<str>} - Master password
72
+ */
73
+ async recoverMasterPassword(vaultName, recoveryData = {}) {
74
+ throw new Error('recoverMasterPassword() must be implemented by subclass');
75
+ }
76
+
77
+ /**
78
+ * Recreate metadata when password change
79
+ * @param {string} vaultName
80
+ * @param {Object} metadata
81
+ * @param {str} oldPassword
82
+ * @param {str} newPassword
83
+ * @returns {Promise<Object>} - new metadata object
84
+ */
85
+ onPasswordChange(vaultName, metadata, oldPassword, newPassword) {
86
+ throw new Error('onPasswordChange() must be implemented by subclass');
87
+ }
88
+ }
@@ -0,0 +1,245 @@
1
+ import crypto from 'crypto';
2
+
3
+ import { IRecoveryMethod, RecoveryData } from './IRecoveryMethod.js';
4
+ import { CryptographyService } from '../CryptographyService.js';
5
+
6
+ export class KeyRecoveryService extends IRecoveryMethod {
7
+ #version = '1.0';
8
+ static BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
9
+ static MIN_KEY_LENGTH = 50;
10
+
11
+ constructor() {
12
+ super();
13
+ this.methodId = 'recoveryKey';
14
+ }
15
+
16
+ /**
17
+ * Get the unique identifier for this recovery method
18
+ * @returns {string} Unique identifier
19
+ */
20
+ getRecoveryMethodId() {
21
+ return this.methodId;
22
+ }
23
+
24
+ /**
25
+ * Check if the metadata is valid
26
+ * @param {string} vaultName
27
+ * @param {Object} metadata
28
+ * @returns {Promise<boolean>}
29
+ */
30
+ isValid(vaultName, metadata) {
31
+ if (
32
+ !!!metadata ||
33
+ !metadata?.salt ||
34
+ !metadata?.encryptedMasterPassword ||
35
+ !metadata?.encryptedRecoveryKey
36
+ ) {
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+
42
+ /**
43
+ * Generate new recovery method
44
+ * @param {string} vaultName
45
+ * @returns {Promise<RecoveryData>} RecoveryData
46
+ */
47
+ async generate(vaultName) {
48
+ return new RecoveryData({ data: { key: this.#generateRecoveryKey() } });
49
+ }
50
+
51
+ /**
52
+ * Create and returns metadata for the recovery option
53
+ * @param {string} vaultName
54
+ * @param {str} masterPassword
55
+ * @param {Object} recoveryData
56
+ * @returns {Promise<Object>} - metadata object
57
+ */
58
+ createMetadata(vaultName, masterPassword, recoveryData = {}) {
59
+ const salt = CryptographyService.generateSalt();
60
+ const recoveryKey = recoveryData.data.key;
61
+ const masterKey = CryptographyService.deriveKey(masterPassword, salt);
62
+ const recoveryKeyDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
63
+ recoveryKey,
64
+ salt
65
+ );
66
+
67
+ const encryptedRecoveryKey = CryptographyService.encrypt(
68
+ { recoveryKey },
69
+ masterKey
70
+ );
71
+ const encryptedMasterPassword = CryptographyService.encrypt(
72
+ { masterPassword },
73
+ recoveryKeyDerived
74
+ );
75
+
76
+ return {
77
+ salt: salt.toString('hex'),
78
+ encryptedRecoveryKey,
79
+ encryptedMasterPassword,
80
+ createdAt: new Date().toISOString(),
81
+ version: this.#version,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Verify recovery data
87
+ * @param {string} vaultName
88
+ * @param {Object} metadata
89
+ * @param {Object} recoveryData
90
+ * @returns {Promise<object>} - Result
91
+ */
92
+ async verify(vaultName, metadata, recoveryData) {
93
+ const result = await this.recoverMasterPassword(
94
+ vaultName,
95
+ metadata,
96
+ recoveryData
97
+ );
98
+ if (result.success) {
99
+ return { success: true };
100
+ } else {
101
+ return result;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * recover master password
107
+ * @param {string} vaultName
108
+ * @param {Object} metadata
109
+ * @param {Object} recoveryData
110
+ * @returns {Promise<str>} - Master password
111
+ */
112
+ async recoverMasterPassword(vaultName, metadata, recoveryData) {
113
+ // TODO: validate metadata/recoveryData
114
+ try {
115
+ const recoveryKey = recoveryData.data.key;
116
+ if (!this.#validateRecoveryKeyFormat(recoveryKey)) {
117
+ return { success: false, error: 'Invalid recovery key format' };
118
+ }
119
+
120
+ const salt = Buffer.from(metadata.salt, 'hex');
121
+ const recoveryKeyDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
122
+ recoveryKey,
123
+ salt
124
+ );
125
+
126
+ try {
127
+ const masterPassword = CryptographyService.decrypt(
128
+ metadata.encryptedMasterPassword,
129
+ recoveryKeyDerived
130
+ );
131
+ return { success: true, ...masterPassword };
132
+ } catch (error) {
133
+ console.error('Invalid recovery key:', error);
134
+ return { success: false, error: `Invalid recovery key: ${error}` };
135
+ }
136
+ } catch (error) {
137
+ console.error('Failed to recover vault using given recovery key:', error);
138
+ return {
139
+ success: false,
140
+ error: 'Failed to recover vault using given recovery key!',
141
+ };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Recreate metadata when password change
147
+ * @param {string} vaultName
148
+ * @param {Object} metadata
149
+ * @param {str} oldPassword
150
+ * @param {str} newPassword
151
+ * @returns {Promise<Object>} - new metadata object
152
+ */
153
+ onPasswordChange(vaultName, metadata, oldPassword, newPassword) {
154
+ const result = this.#loadRecoveryKey(vaultName, metadata, oldPassword);
155
+ if (!result.success) {
156
+ console.warn(`Unable to load recovery key for vault: ${vaultName}.`);
157
+ console.log(
158
+ 'Looks like the recovery key does not exist or already corrupted!'
159
+ );
160
+ return { success: false, metadata };
161
+ }
162
+
163
+ try {
164
+ const recoveryData = new RecoveryData({
165
+ data: { key: result.recoveryKey },
166
+ });
167
+ const newMetadata = this.createMetadata(
168
+ vaultName,
169
+ newPassword,
170
+ recoveryData
171
+ );
172
+ return { success: true, metadata: newMetadata };
173
+ } catch (error) {
174
+ console.error('Failed to create metadata:', error);
175
+ return { success: false, error: 'Failed to create metadata' };
176
+ }
177
+ }
178
+
179
+ #loadRecoveryKey(vaultName, metadata, masterPassword) {
180
+ // TODO: validate metadata
181
+ try {
182
+ const salt = Buffer.from(metadata.salt, 'hex');
183
+ const key = CryptographyService.deriveKey(masterPassword, salt);
184
+
185
+ try {
186
+ const recoveryKey = CryptographyService.decrypt(
187
+ metadata.encryptedRecoveryKey,
188
+ key
189
+ );
190
+ return { success: true, ...recoveryKey };
191
+ } catch (error) {
192
+ console.error('Invalid recovery key:', error);
193
+ return { success: false, error: `Invalid recovery key: ${error}` };
194
+ }
195
+ } catch (error) {
196
+ console.error(
197
+ 'Internal error: Look like the recovery key is corrupted:',
198
+ error
199
+ );
200
+ return {
201
+ success: false,
202
+ error: 'Internal error: Look like the recovery key is corrupted.',
203
+ };
204
+ }
205
+ }
206
+
207
+ #generateRecoveryKey() {
208
+ const recoveryKeyBytes = crypto.randomBytes(32);
209
+
210
+ let result = '';
211
+ let bits = 0;
212
+ let value = 0;
213
+
214
+ for (let i = 0; i < recoveryKeyBytes.length; i++) {
215
+ value = (value << 8) | recoveryKeyBytes[i];
216
+ bits += 8;
217
+
218
+ while (bits >= 5) {
219
+ result +=
220
+ KeyRecoveryService.BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
221
+ bits -= 5;
222
+ }
223
+ }
224
+
225
+ if (bits > 0) {
226
+ result += KeyRecoveryService.BASE32_ALPHABET[(value << (5 - bits)) & 31];
227
+ }
228
+
229
+ return result.match(/.{1,4}/g).join('-');
230
+ }
231
+
232
+ #validateRecoveryKeyFormat(recoveryKey) {
233
+ const cleanKey = recoveryKey.replace(/-/g, '').toUpperCase();
234
+ const base32Regex = /^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/;
235
+ return (
236
+ base32Regex.test(cleanKey) &&
237
+ cleanKey.length >= KeyRecoveryService.MIN_KEY_LENGTH
238
+ );
239
+ }
240
+
241
+ static deriveKeyFromRecoveryKey(recoveryKey, salt) {
242
+ const cleanKey = recoveryKey.replace(/-/g, '').toUpperCase();
243
+ return CryptographyService.deriveKey(cleanKey, salt);
244
+ }
245
+ }