@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,578 @@
1
+ import { Vault } from '../models/Vault.js';
2
+ import { CryptographyService } from './CryptographyService.js';
3
+ import { VaultFileService } from './VaultFileService.js';
4
+ import { validatePasswordStrength } from '../utils/passwordValidation.js';
5
+ import { KeyRecoveryService } from './recovery/KeyRecoveryService.js';
6
+ import { PasswordRecoveryService } from './recovery/PasswordRecoveryService.js';
7
+ import { RecoveryData } from './recovery/IRecoveryMethod.js';
8
+
9
+ export class VaultService {
10
+ constructor(vaultDirectory) {
11
+ this.fileService = new VaultFileService(vaultDirectory);
12
+ }
13
+
14
+ async getAvailableVaults() {
15
+ const vaults = await this.fileService.listVaults();
16
+
17
+ // Ensure default vault exists
18
+ if (!vaults.includes('default')) {
19
+ await this.createDefaultVault();
20
+ vaults.unshift('default');
21
+ }
22
+
23
+ return vaults;
24
+ }
25
+
26
+ async verifyVaultPassword(vaultName, password) {
27
+ try {
28
+ // Try to load the vault with the provided password
29
+ const result = await this.loadVault(vaultName, password);
30
+ return {
31
+ success: result.success,
32
+ valid: result.success && !!result.data,
33
+ error: result.error,
34
+ };
35
+ } catch (error) {
36
+ console.error(`Error verifying password for vault ${vaultName}:`, error);
37
+ return {
38
+ success: false,
39
+ valid: false,
40
+ error: error.message,
41
+ };
42
+ }
43
+ }
44
+
45
+ async encryptVault(data, masterPassword, recoveryKey, oldPassword) {
46
+ const salt = CryptographyService.generateSalt();
47
+ const key = CryptographyService.deriveKey(masterPassword, salt);
48
+
49
+ // Generate recovery key
50
+ let recoveryMetadata = RecoveryKeyService.createRecoveryMetadata(
51
+ recoveryKey,
52
+ masterPassword,
53
+ salt
54
+ );
55
+
56
+ if (oldPassword) {
57
+ const currentKey = CryptographyService.deriveKey(oldPassword, salt);
58
+ const recoveryPassword = CryptographyService.encrypt(
59
+ { masterPassword },
60
+ currentKey
61
+ );
62
+ recoveryMetadata = {
63
+ ...recoveryMetadata,
64
+ recoveryPassword,
65
+ };
66
+ }
67
+
68
+ const encryptedData = CryptographyService.encrypt(vault.toJSON(), key);
69
+ return {
70
+ ...encryptedData,
71
+ salt: salt.toString('hex'),
72
+ recoveryMetadata,
73
+ };
74
+ }
75
+
76
+ async createVault(vaultName, masterPassword) {
77
+ if (await this.fileService.vaultExists(vaultName)) {
78
+ throw new Error('Vault already exists');
79
+ }
80
+
81
+ const vault = new Vault({ name: vaultName });
82
+ const salt = CryptographyService.generateSalt();
83
+ const key = CryptographyService.deriveKey(masterPassword, salt);
84
+
85
+ // Generate recovery key
86
+ const keyRecoveryService = new KeyRecoveryService();
87
+ const recoveryKey = await keyRecoveryService.generate();
88
+ const recoveryKeyMetadata = keyRecoveryService.createMetadata(
89
+ vaultName,
90
+ masterPassword,
91
+ recoveryKey
92
+ );
93
+
94
+ const encryptedData = CryptographyService.encrypt(vault.toJSON(), key);
95
+ let recoveryMetadata = {};
96
+ recoveryMetadata[keyRecoveryService.getRecoveryMethodId()] =
97
+ recoveryKeyMetadata;
98
+ const vaultFile = {
99
+ ...encryptedData,
100
+ salt: salt.toString('hex'),
101
+ recoveryMetadata,
102
+ };
103
+
104
+ await this.fileService.writeVaultFile(vaultName, vaultFile);
105
+
106
+ return {
107
+ success: true,
108
+ recoveryKey,
109
+ recoveryKeyCreatedAt: recoveryMetadata.recoveryKey.createdAt,
110
+ };
111
+ }
112
+
113
+ async verifyPassword(vaultName, password, vaultPath = null) {
114
+ if (!!!vaultPath) {
115
+ vaultPath = this.fileService.getVaultPath(vaultName);
116
+ }
117
+ try {
118
+ const vaultFile = await this.fileService.readVaultPath(vaultPath);
119
+ const salt = Buffer.from(vaultFile.salt, 'hex');
120
+ const key = CryptographyService.deriveKey(password, salt);
121
+
122
+ const encryptedData = {
123
+ encrypted: vaultFile.encrypted,
124
+ authTag: vaultFile.authTag,
125
+ iv: vaultFile.iv,
126
+ };
127
+
128
+ CryptographyService.decrypt(encryptedData, key);
129
+ return { success: true };
130
+ } catch (error) {
131
+ return { success: false, error: 'Invalid master password' };
132
+ }
133
+ }
134
+
135
+ async loadVault(vaultName, password) {
136
+ try {
137
+ console.log(`[VaultService] Loading vault: ${vaultName}`);
138
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
139
+ console.log(
140
+ '[VaultService] Vault file loaded:',
141
+ JSON.stringify(vaultFile, null, 2)
142
+ );
143
+
144
+ const salt = Buffer.from(vaultFile.salt, 'hex');
145
+ const key = CryptographyService.deriveKey(password, salt);
146
+
147
+ const encryptedData = {
148
+ encrypted: vaultFile.encrypted,
149
+ authTag: vaultFile.authTag,
150
+ iv: vaultFile.iv,
151
+ };
152
+
153
+ const vaultData = CryptographyService.decrypt(encryptedData, key);
154
+
155
+ const vault = Vault.fromJSON(vaultData, vaultName);
156
+ const vaultJson = vault.toJSON();
157
+ return { success: true, data: vaultJson };
158
+ } catch (error) {
159
+ return { success: false, error: error.message };
160
+ }
161
+ }
162
+
163
+ async saveVault(vaultName, password, vaultData) {
164
+ try {
165
+ // Load existing recovery metadata
166
+ let existingRecoveryMetadata = {};
167
+ if (await this.fileService.vaultExists(vaultName)) {
168
+ try {
169
+ const existingFile = await this.fileService.readVaultFile(vaultName);
170
+ existingRecoveryMetadata = existingFile.recoveryMetadata || {};
171
+ } catch (error) {
172
+ console.warn('Could not load existing recovery metadata');
173
+ }
174
+ }
175
+
176
+ const vault = Vault.fromJSON(vaultData, vaultName);
177
+ const salt = CryptographyService.generateSalt();
178
+ const key = CryptographyService.deriveKey(password, salt);
179
+
180
+ const encryptedData = CryptographyService.encrypt(vault.toJSON(), key);
181
+ const vaultFile = {
182
+ ...encryptedData,
183
+ salt: salt.toString('hex'),
184
+ recoveryMetadata: existingRecoveryMetadata,
185
+ };
186
+
187
+ await this.fileService.writeVaultFile(vaultName, vaultFile);
188
+ return { success: true };
189
+ } catch (error) {
190
+ return { success: false, error: error.message };
191
+ }
192
+ }
193
+
194
+ async changePassword(vaultName, currentPassword, newPassword) {
195
+ try {
196
+ // Validate new password
197
+ const passwordErrors = validatePasswordStrength(newPassword);
198
+ if (passwordErrors.length > 0) {
199
+ return { success: false, error: passwordErrors[0] };
200
+ }
201
+
202
+ if (!(await this.fileService.vaultExists(vaultName))) {
203
+ return { success: false, error: 'Vault not found' };
204
+ }
205
+
206
+ // Create backup
207
+ await this.fileService.createBackup(vaultName);
208
+
209
+ try {
210
+ // Load and verify current password
211
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
212
+ const currentSalt = Buffer.from(vaultFile.salt, 'hex');
213
+ const currentKey = CryptographyService.deriveKey(
214
+ currentPassword,
215
+ currentSalt
216
+ );
217
+
218
+ const encryptedData = {
219
+ encrypted: vaultFile.encrypted,
220
+ authTag: vaultFile.authTag,
221
+ iv: vaultFile.iv,
222
+ };
223
+
224
+ const vaultData = CryptographyService.decrypt(
225
+ encryptedData,
226
+ currentKey
227
+ );
228
+ const vault = Vault.fromJSON(vaultData, vaultName);
229
+
230
+ // Check password reuse
231
+ const newPasswordHash = CryptographyService.hashPassword(newPassword);
232
+ if (newPassword === currentPassword) {
233
+ return {
234
+ success: false,
235
+ error: 'New password must be different from current password',
236
+ };
237
+ }
238
+
239
+ if (vault.checkPasswordReuse(newPasswordHash)) {
240
+ return {
241
+ success: false,
242
+ error:
243
+ 'This password has been used before. Please choose a different password.',
244
+ };
245
+ }
246
+
247
+ // Update vault with password history
248
+ const currentPasswordHash =
249
+ CryptographyService.hashPassword(currentPassword);
250
+ vault.addPasswordToHistory(currentPasswordHash);
251
+ vault.updateLastPasswordChange();
252
+
253
+ // Update recovery metadata
254
+ const updatedRecoveryMetadata =
255
+ await this.#updateRecoveryMetadataForPasswordChange(
256
+ vaultName,
257
+ vaultFile.recoveryMetadata || {},
258
+ currentPassword,
259
+ newPassword
260
+ );
261
+
262
+ // Re-encrypt with new password
263
+ const newSalt = CryptographyService.generateSalt();
264
+ const newKey = CryptographyService.deriveKey(newPassword, newSalt);
265
+ const newEncryptedData = CryptographyService.encrypt(
266
+ vault.toJSON(),
267
+ newKey
268
+ );
269
+
270
+ const finalVaultFile = {
271
+ ...newEncryptedData,
272
+ salt: newSalt.toString('hex'),
273
+ recoveryMetadata: updatedRecoveryMetadata,
274
+ };
275
+
276
+ // Test decryption before saving
277
+ const testKey = CryptographyService.deriveKey(newPassword, newSalt);
278
+ const testEncryptedData = {
279
+ encrypted: finalVaultFile.encrypted,
280
+ authTag: finalVaultFile.authTag,
281
+ iv: finalVaultFile.iv,
282
+ };
283
+ CryptographyService.decrypt(testEncryptedData, testKey);
284
+
285
+ // Atomically write the new vault file
286
+ await this.fileService.atomicWriteVaultFile(vaultName, finalVaultFile);
287
+
288
+ return { success: true };
289
+ } catch (error) {
290
+ // Restore from backup on error
291
+ await this.fileService.restoreFromBackup(vaultName);
292
+ throw error;
293
+ }
294
+ } catch (error) {
295
+ console.error('Error changing password:', error);
296
+ return { success: false, error: 'Failed to change master password' };
297
+ }
298
+ }
299
+
300
+ async deleteVault(vaultName, confirmationPassword = null) {
301
+ try {
302
+ if (!(await this.fileService.vaultExists(vaultName))) {
303
+ return { success: false, error: 'Vault not found' };
304
+ }
305
+
306
+ if (vaultName === 'default' && !confirmationPassword) {
307
+ return {
308
+ success: false,
309
+ error: 'Cannot delete default vault without password confirmation',
310
+ };
311
+ }
312
+
313
+ // Verify password if provided
314
+ if (confirmationPassword) {
315
+ const verification = await this.verifyPassword(
316
+ vaultName,
317
+ confirmationPassword
318
+ );
319
+ if (!verification.success) {
320
+ return {
321
+ success: false,
322
+ error: 'Invalid password. Vault not deleted.',
323
+ };
324
+ }
325
+ }
326
+
327
+ const backupFile = await this.fileService.deleteVault(vaultName);
328
+
329
+ return {
330
+ success: true,
331
+ message: `Vault "${vaultName}" has been deleted. A backup was created.`,
332
+ backupFile,
333
+ };
334
+ } catch (error) {
335
+ return { success: false, error: 'Failed to delete vault' };
336
+ }
337
+ }
338
+
339
+ async createDefaultVault() {
340
+ const defaultPassword = 'changeme123';
341
+ const result = await this.createVault('default', defaultPassword);
342
+
343
+ if (result.success) {
344
+ console.log(
345
+ 'Default vault created with recovery key:',
346
+ result.recoveryKey
347
+ );
348
+ }
349
+
350
+ return result;
351
+ }
352
+
353
+ // Recovery methods
354
+ async generateRecoveryKey(vaultName, masterPassword) {
355
+ try {
356
+ if (!(await this.fileService.vaultExists(vaultName))) {
357
+ return { success: false, error: 'Vault not found' };
358
+ }
359
+
360
+ // Verify password
361
+ const verification = await this.verifyPassword(vaultName, masterPassword);
362
+ if (!verification.success) {
363
+ return { success: false, error: 'Invalid master password' };
364
+ }
365
+
366
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
367
+
368
+ const keyRecoveryService = new KeyRecoveryService();
369
+ const recoveryKey = await keyRecoveryService.generate();
370
+ const recoveryMetadata = keyRecoveryService.createMetadata(
371
+ vaultName,
372
+ masterPassword,
373
+ recoveryKey
374
+ );
375
+ let newRecoveryMetadata = vaultFile.recoveryMetadata;
376
+ newRecoveryMetadata[keyRecoveryService.getRecoveryMethodId()] =
377
+ recoveryMetadata;
378
+
379
+ // Update vault file
380
+ const updatedVaultFile = {
381
+ ...vaultFile,
382
+ recoveryMetadata: newRecoveryMetadata,
383
+ };
384
+
385
+ await this.fileService.writeVaultFile(vaultName, updatedVaultFile);
386
+
387
+ return {
388
+ success: true,
389
+ recoveryKey: recoveryKey.data.key,
390
+ createdAt: recoveryMetadata.createdAt,
391
+ };
392
+ } catch (error) {
393
+ console.error('Failed to generate recovery key: ', error);
394
+ return { success: false, error: 'Failed to generate recovery key' };
395
+ }
396
+ }
397
+
398
+ async verifyRecoveryKey(vaultName, recoveryKey) {
399
+ try {
400
+ if (!(await this.fileService.vaultExists(vaultName))) {
401
+ return { success: false, error: 'Vault not found' };
402
+ }
403
+
404
+ const keyRecoveryService = new KeyRecoveryService();
405
+ const methodId = keyRecoveryService.getRecoveryMethodId();
406
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
407
+
408
+ if (
409
+ !vaultFile.recoveryMetadata ||
410
+ !vaultFile.recoveryMetadata[methodId]
411
+ ) {
412
+ return {
413
+ success: false,
414
+ error: 'No recovery key found for this vault',
415
+ };
416
+ }
417
+
418
+ const recoveryData = new RecoveryData({ data: { key: recoveryKey } });
419
+ return await keyRecoveryService.verify(
420
+ vaultName,
421
+ vaultFile.recoveryMetadata[methodId],
422
+ recoveryData
423
+ );
424
+ } catch (error) {
425
+ console.error(`Failed to verify recovery key: ${error}`);
426
+ return { success: false, error: 'Failed to verify recovery key' };
427
+ }
428
+ }
429
+
430
+ async loadVaultWithPassword(vaultName, oldPassword) {
431
+ try {
432
+ if (!(await this.fileService.vaultExists(vaultName))) {
433
+ return { success: false, error: 'Vault not found' };
434
+ }
435
+
436
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
437
+
438
+ // First, try if the old password is actually the current password
439
+ try {
440
+ const loadResult = await this.loadVault(vaultName, oldPassword);
441
+
442
+ if (loadResult.success) {
443
+ loadResult.password = oldPassword;
444
+ return loadResult;
445
+ }
446
+ } catch (error) {
447
+ // Not the current password, try recovery
448
+ }
449
+
450
+ const passwordRecoveryService = new PasswordRecoveryService();
451
+ const methodId = passwordRecoveryService.getRecoveryMethodId();
452
+ // Check if we have previous password recovery data
453
+ if (
454
+ !vaultFile.recoveryMetadata ||
455
+ !vaultFile.recoveryMetadata[methodId]
456
+ ) {
457
+ return {
458
+ success: false,
459
+ error:
460
+ 'No recovery data available for this vault. You need to change your password at least once to enable previous password recovery.',
461
+ };
462
+ }
463
+
464
+ try {
465
+ const recoveryData = new RecoveryData({
466
+ data: { password: oldPassword },
467
+ });
468
+ const result = await passwordRecoveryService.recoverMasterPassword(
469
+ vaultName,
470
+ vaultFile.recoveryMetadata[methodId],
471
+ recoveryData
472
+ );
473
+ if (!result.success) {
474
+ return result;
475
+ }
476
+
477
+ const masterPassword = result.masterPassword;
478
+ const loadResult = await this.loadVault(vaultName, masterPassword);
479
+
480
+ if (loadResult.success) {
481
+ loadResult.password = masterPassword;
482
+ }
483
+
484
+ return loadResult;
485
+ } catch (error) {
486
+ const message =
487
+ 'Failed to recover vault using recovered master password';
488
+ console.error(`${message}: ${error}`);
489
+ return { success: false, error: message };
490
+ }
491
+ } catch (error) {
492
+ const message = 'Failed to recover vault with old password';
493
+ console.error(message, ': ', error);
494
+ return { success: false, error: message };
495
+ }
496
+ }
497
+
498
+ async loadVaultWithRecoveryKey(vaultName, recoveryKey) {
499
+ try {
500
+ if (!(await this.fileService.vaultExists(vaultName))) {
501
+ return { success: false, error: 'Vault not found' };
502
+ }
503
+
504
+ const keyRecoveryService = new KeyRecoveryService();
505
+ const methodId = keyRecoveryService.getRecoveryMethodId();
506
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
507
+
508
+ if (
509
+ !vaultFile.recoveryMetadata ||
510
+ !vaultFile.recoveryMetadata[methodId]
511
+ ) {
512
+ return {
513
+ success: false,
514
+ error: 'No recovery key found for this vault',
515
+ };
516
+ }
517
+
518
+ const recoveryData = new RecoveryData({ data: { key: recoveryKey } });
519
+ const result = await keyRecoveryService.recoverMasterPassword(
520
+ vaultName,
521
+ vaultFile.recoveryMetadata[methodId],
522
+ recoveryData
523
+ );
524
+
525
+ if (!result.success) {
526
+ return result;
527
+ }
528
+
529
+ const masterPassword = result.masterPassword;
530
+ const loadResult = await this.loadVault(vaultName, masterPassword);
531
+
532
+ if (loadResult.success) {
533
+ loadResult.password = masterPassword;
534
+ }
535
+
536
+ return loadResult;
537
+ } catch (error) {
538
+ console.error(`Failed to load vault with recovery key: ${error}`);
539
+ return {
540
+ success: false,
541
+ error: 'Failed to load vault with recovery key',
542
+ };
543
+ }
544
+ }
545
+
546
+ async #updateRecoveryMetadataForPasswordChange(
547
+ vaultName,
548
+ existingMetadata,
549
+ currentPassword,
550
+ newPassword
551
+ ) {
552
+ const keyRecoveryService = new KeyRecoveryService();
553
+ const passwordRecoveryService = new PasswordRecoveryService();
554
+
555
+ const metadata = [keyRecoveryService, passwordRecoveryService]
556
+ .flatMap((recoveryMethod) => {
557
+ const methodId = recoveryMethod.getRecoveryMethodId();
558
+ const recoveryMetadata = existingMetadata[methodId] || {};
559
+ const result = recoveryMethod.onPasswordChange(
560
+ vaultName,
561
+ recoveryMetadata,
562
+ currentPassword,
563
+ newPassword
564
+ );
565
+ // By default let's return old data, so that we could recover it later if possible
566
+ return {
567
+ methodId,
568
+ data: result.success ? result.metadata : recoveryMetadata,
569
+ };
570
+ })
571
+ .reduce((obj, item) => {
572
+ obj[item.methodId] = item.data;
573
+ return obj;
574
+ }, {});
575
+
576
+ return metadata;
577
+ }
578
+ }
@@ -0,0 +1,78 @@
1
+ import { VaultFileService } from './VaultFileService.js';
2
+ import { CryptographyService } from './CryptographyService.js';
3
+ import { Vault } from '../models/Vault.js';
4
+
5
+ export class VaultSettingsService {
6
+ constructor(vaultDirectory) {
7
+ this.fileService = new VaultFileService(vaultDirectory);
8
+ }
9
+
10
+ async updateVaultSettings(vaultName, vaultPassword, newSettings) {
11
+ try {
12
+ if (!(await this.fileService.vaultExists(vaultName))) {
13
+ return { success: false, error: 'Vault not found' };
14
+ }
15
+
16
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
17
+ const salt = Buffer.from(vaultFile.salt, 'hex');
18
+ const key = CryptographyService.deriveKey(vaultPassword, salt);
19
+
20
+ let vaultData;
21
+ try {
22
+ const encryptedData = {
23
+ encrypted: vaultFile.encrypted,
24
+ authTag: vaultFile.authTag,
25
+ iv: vaultFile.iv,
26
+ };
27
+ vaultData = CryptographyService.decrypt(encryptedData, key);
28
+ } catch (error) {
29
+ return { success: false, error: 'Invalid password' };
30
+ }
31
+
32
+ const vault = Vault.fromJSON(vaultData, vaultName);
33
+ vault.updateSettings(newSettings);
34
+
35
+ const newEncryptedData = CryptographyService.encrypt(vault.toJSON(), key);
36
+ const finalData = {
37
+ ...newEncryptedData,
38
+ salt: salt.toString('hex'),
39
+ recoveryMetadata: vaultFile.recoveryMetadata || {},
40
+ };
41
+
42
+ await this.fileService.writeVaultFile(vaultName, finalData);
43
+ return { success: true };
44
+ } catch (error) {
45
+ console.error('Error updating vault settings:', error);
46
+ return { success: false, error: 'Failed to update settings' };
47
+ }
48
+ }
49
+
50
+ async getVaultSettings(vaultName, vaultPassword) {
51
+ try {
52
+ if (!(await this.fileService.vaultExists(vaultName))) {
53
+ return { success: false, error: 'Vault not found' };
54
+ }
55
+
56
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
57
+ const salt = Buffer.from(vaultFile.salt, 'hex');
58
+ const key = CryptographyService.deriveKey(vaultPassword, salt);
59
+
60
+ try {
61
+ const encryptedData = {
62
+ encrypted: vaultFile.encrypted,
63
+ authTag: vaultFile.authTag,
64
+ iv: vaultFile.iv,
65
+ };
66
+ const vaultData = CryptographyService.decrypt(encryptedData, key);
67
+ const vault = Vault.fromJSON(vaultData, vaultName);
68
+
69
+ return { success: true, settings: vault.settings };
70
+ } catch (error) {
71
+ return { success: false, error: 'Invalid password' };
72
+ }
73
+ } catch (error) {
74
+ console.error('Error getting vault settings:', error);
75
+ return { success: false, error: 'Failed to get settings' };
76
+ }
77
+ }
78
+ }