@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,564 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import { CryptographyService } from './CryptographyService.js';
5
+ import { EnvironmentVault } from '../models/EnvironmentVault.js';
6
+ import { validatePasswordStrength } from '../utils/passwordValidation.js';
7
+ import { getAppDataPath, getEnvsDir } from '../utils/appPaths.js';
8
+
9
+ function resolvePath(...segments) {
10
+ return process.platform === 'win32'
11
+ ? path.win32.join(...segments)
12
+ : path.posix.join(...segments);
13
+ }
14
+
15
+ export class EnvironmentVaultService {
16
+ static getAppDataPath() {
17
+ return getAppDataPath();
18
+ }
19
+
20
+ static getEnvsDir() {
21
+ return getEnvsDir();
22
+ }
23
+
24
+ static getEnvVaultPath(name) {
25
+ return resolvePath(this.getEnvsDir(), `${name}.env.vault`);
26
+ }
27
+
28
+ static getBackupPath(name) {
29
+ return resolvePath(this.getEnvsDir(), `${name}.env.vault.bak`);
30
+ }
31
+
32
+ static resolveVaultPath({ vault, name } = {}) {
33
+ if (vault) {
34
+ return path.resolve(vault);
35
+ }
36
+
37
+ if (name) {
38
+ return this.getEnvVaultPath(name);
39
+ }
40
+
41
+ const cwdName = path
42
+ .basename(process.cwd())
43
+ .replace(/[^a-z0-9_-]/gi, '_')
44
+ .toLowerCase();
45
+
46
+ const localVault = path.resolve('.env.vault');
47
+ if (fs.existsSync(localVault)) {
48
+ return localVault;
49
+ }
50
+
51
+ const configVault = path.resolve('config', '.env.vault');
52
+ if (fs.existsSync(configVault)) {
53
+ return configVault;
54
+ }
55
+
56
+ const appDataVault = this.getEnvVaultPath(cwdName);
57
+ if (fs.existsSync(appDataVault)) {
58
+ return appDataVault;
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ static defaultVaultPath() {
65
+ const cwdName = path
66
+ .basename(process.cwd())
67
+ .replace(/[^a-z0-9_-]/gi, '_')
68
+ .toLowerCase();
69
+ return this.getEnvVaultPath(cwdName);
70
+ }
71
+
72
+ static async vaultExists(path) {
73
+ try {
74
+ return await fs.pathExists(path);
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ static async createVault(vaultPath, password, data = null) {
81
+ const passwordErrors = validatePasswordStrength(password);
82
+ if (passwordErrors.length > 0) {
83
+ return { success: false, error: passwordErrors[0] };
84
+ }
85
+
86
+ try {
87
+ if (await this.vaultExists(vaultPath)) {
88
+ return {
89
+ success: false,
90
+ error: `Vault already exists at ${vaultPath}`,
91
+ };
92
+ }
93
+
94
+ const payload = data || new EnvironmentVault().toJSON();
95
+ const salt = CryptographyService.generateSalt();
96
+ const key = CryptographyService.deriveKey(password, salt);
97
+ const encrypted = CryptographyService.encrypt(payload, key);
98
+
99
+ const vaultFile = {
100
+ type: 'environment-vault',
101
+ version: 1,
102
+ salt: salt.toString('hex'),
103
+ iv: encrypted.iv,
104
+ authTag: encrypted.authTag,
105
+ encrypted: encrypted.encrypted,
106
+ };
107
+
108
+ await fs.ensureDir(path.dirname(vaultPath));
109
+ await fs.writeJSON(vaultPath, vaultFile, { spaces: 2 });
110
+
111
+ return { success: true, path: vaultPath };
112
+ } catch (error) {
113
+ return { success: false, error: error.message };
114
+ }
115
+ }
116
+
117
+ static async loadVault(vaultPath, password) {
118
+ try {
119
+ if (!(await this.vaultExists(vaultPath))) {
120
+ return {
121
+ success: false,
122
+ error: `Environment vault not found at ${vaultPath}`,
123
+ };
124
+ }
125
+
126
+ const vaultFile = await fs.readJSON(vaultPath);
127
+
128
+ if (vaultFile.type !== 'environment-vault') {
129
+ return { success: false, error: 'Invalid environment vault file' };
130
+ }
131
+
132
+ const salt = Buffer.from(vaultFile.salt, 'hex');
133
+ const key = CryptographyService.deriveKey(password, salt);
134
+ const encryptedData = {
135
+ encrypted: vaultFile.encrypted,
136
+ authTag: vaultFile.authTag,
137
+ iv: vaultFile.iv,
138
+ };
139
+
140
+ const payload = CryptographyService.decrypt(encryptedData, key);
141
+ const vault = EnvironmentVault.fromJSON(payload);
142
+
143
+ return { success: true, data: vault };
144
+ } catch (error) {
145
+ return {
146
+ success: false,
147
+ error: 'Failed to decrypt: wrong password or corrupted file',
148
+ };
149
+ }
150
+ }
151
+
152
+ static async saveVault(vaultPath, password, vault) {
153
+ try {
154
+ const payload = vault.toJSON();
155
+ const salt = CryptographyService.generateSalt();
156
+ const key = CryptographyService.deriveKey(password, salt);
157
+ const encrypted = CryptographyService.encrypt(payload, key);
158
+
159
+ const vaultFile = {
160
+ type: 'environment-vault',
161
+ version: 1,
162
+ salt: salt.toString('hex'),
163
+ iv: encrypted.iv,
164
+ authTag: encrypted.authTag,
165
+ encrypted: encrypted.encrypted,
166
+ };
167
+
168
+ await fs.ensureDir(path.dirname(vaultPath));
169
+ await fs.writeJSON(vaultPath, vaultFile, { spaces: 2 });
170
+
171
+ return { success: true };
172
+ } catch (error) {
173
+ return { success: false, error: error.message };
174
+ }
175
+ }
176
+
177
+ static async changePassword(vaultPath, currentPassword, newPassword) {
178
+ const passwordErrors = validatePasswordStrength(newPassword);
179
+ if (passwordErrors.length > 0) {
180
+ return { success: false, error: passwordErrors[0] };
181
+ }
182
+
183
+ const loadResult = await this.loadVault(vaultPath, currentPassword);
184
+ if (!loadResult.success) {
185
+ return loadResult;
186
+ }
187
+
188
+ return this.saveVault(vaultPath, newPassword, loadResult.data);
189
+ }
190
+
191
+ static async init({ name, vault, password, environments = {} }) {
192
+ const vaultPath = vault
193
+ ? path.resolve(vault)
194
+ : name
195
+ ? this.getEnvVaultPath(name)
196
+ : this.defaultVaultPath();
197
+
198
+ if (!password) {
199
+ return { success: false, error: 'Password is required' };
200
+ }
201
+
202
+ const vaultModel = new EnvironmentVault();
203
+
204
+ for (const [envName, envFile] of Object.entries(environments)) {
205
+ try {
206
+ const content = await fs.readFile(envFile, 'utf-8');
207
+ vaultModel.importFromEnvFile(envName, content, {
208
+ message: 'Initial import',
209
+ });
210
+ } catch (error) {
211
+ return {
212
+ success: false,
213
+ error: `Failed to import ${envName} from ${envFile}: ${error.message}`,
214
+ };
215
+ }
216
+ }
217
+
218
+ return this.createVault(vaultPath, password, vaultModel.toJSON());
219
+ }
220
+
221
+ static async setEnv(
222
+ vaultPath,
223
+ password,
224
+ envName,
225
+ key,
226
+ value,
227
+ { isPublic = false, message = null } = {}
228
+ ) {
229
+ const loadResult = await this.loadVault(vaultPath, password);
230
+ if (!loadResult.success) return loadResult;
231
+
232
+ const vault = loadResult.data;
233
+
234
+ if (!vault.listEnvironmentNames().includes(envName)) {
235
+ vault.addEnvironment(envName);
236
+ }
237
+
238
+ const activeVersion = vault.getActiveVersion(envName);
239
+
240
+ const nonSensitive = activeVersion ? [...activeVersion.nonSensitive] : [];
241
+ const required = activeVersion ? [...activeVersion.required] : [];
242
+
243
+ if (isPublic && !nonSensitive.includes(key)) {
244
+ nonSensitive.push(key);
245
+ } else if (!isPublic) {
246
+ const idx = nonSensitive.indexOf(key);
247
+ if (idx !== -1) nonSensitive.splice(idx, 1);
248
+ }
249
+
250
+ const currentVars = activeVersion ? { ...activeVersion.vars } : {};
251
+ currentVars[key] = value;
252
+
253
+ vault.addVersion(envName, currentVars, { nonSensitive, required, message });
254
+
255
+ return this.saveVault(vaultPath, password, vault);
256
+ }
257
+
258
+ static async getEnv(vaultPath, password, envName, key) {
259
+ const loadResult = await this.loadVault(vaultPath, password);
260
+ if (!loadResult.success) return loadResult;
261
+
262
+ try {
263
+ const vault = loadResult.data;
264
+ const activeVersion = vault.getActiveVersion(envName);
265
+
266
+ if (!activeVersion) {
267
+ return {
268
+ success: false,
269
+ error: `Environment '${envName}' has no versions`,
270
+ };
271
+ }
272
+
273
+ if (!(key in activeVersion.vars)) {
274
+ return {
275
+ success: false,
276
+ error: `Key '${key}' not found in environment '${envName}'`,
277
+ };
278
+ }
279
+
280
+ return { success: true, data: { key, value: activeVersion.vars[key] } };
281
+ } catch (error) {
282
+ return { success: false, error: error.message };
283
+ }
284
+ }
285
+
286
+ static async showEnv(vaultPath, password, envName) {
287
+ const loadResult = await this.loadVault(vaultPath, password);
288
+ if (!loadResult.success) return loadResult;
289
+
290
+ try {
291
+ const vault = loadResult.data;
292
+ const activeVersion = vault.getActiveVersion(envName);
293
+
294
+ if (!activeVersion) {
295
+ return {
296
+ success: false,
297
+ error: `Environment '${envName}' has no versions`,
298
+ };
299
+ }
300
+
301
+ const keys = Object.entries(activeVersion.vars).map(([key, value]) => ({
302
+ key,
303
+ value,
304
+ sensitive: !activeVersion.nonSensitive.includes(key),
305
+ }));
306
+
307
+ return {
308
+ success: true,
309
+ data: {
310
+ name: envName,
311
+ activeVersion: activeVersion.n,
312
+ totalVersions: vault.getHistory(envName).length,
313
+ keyCount: keys.length,
314
+ keys,
315
+ },
316
+ };
317
+ } catch (error) {
318
+ return { success: false, error: error.message };
319
+ }
320
+ }
321
+
322
+ static async listEnvs(vaultPath, password) {
323
+ const loadResult = await this.loadVault(vaultPath, password);
324
+ if (!loadResult.success) return loadResult;
325
+
326
+ const vault = loadResult.data;
327
+ const names = vault.listEnvironmentNames();
328
+
329
+ const envs = names.map((name) => {
330
+ const history = vault.getHistory(name);
331
+ const activeVersion = vault.getActiveVersion(name);
332
+ return {
333
+ name,
334
+ versionCount: history.length,
335
+ activeVersion: activeVersion ? activeVersion.n : null,
336
+ keyCount: activeVersion ? Object.keys(activeVersion.vars).length : 0,
337
+ };
338
+ });
339
+
340
+ return { success: true, data: envs };
341
+ }
342
+
343
+ static async removeKey(vaultPath, password, envName, key) {
344
+ const loadResult = await this.loadVault(vaultPath, password);
345
+ if (!loadResult.success) return loadResult;
346
+
347
+ try {
348
+ const vault = loadResult.data;
349
+ const activeVersion = vault.getActiveVersion(envName);
350
+
351
+ if (!activeVersion) {
352
+ return {
353
+ success: false,
354
+ error: `Environment '${envName}' has no versions`,
355
+ };
356
+ }
357
+
358
+ if (!(key in activeVersion.vars)) {
359
+ return {
360
+ success: false,
361
+ error: `Key '${key}' not found in environment '${envName}'`,
362
+ };
363
+ }
364
+
365
+ const newVars = { ...activeVersion.vars };
366
+ delete newVars[key];
367
+
368
+ const nonSensitive = activeVersion.nonSensitive.filter((k) => k !== key);
369
+ const required = activeVersion.required.filter((k) => k !== key);
370
+
371
+ vault.addVersion(envName, newVars, {
372
+ nonSensitive,
373
+ required,
374
+ message: `Remove ${key}`,
375
+ });
376
+
377
+ return this.saveVault(vaultPath, password, vault);
378
+ } catch (error) {
379
+ return { success: false, error: error.message };
380
+ }
381
+ }
382
+
383
+ static async deleteEnv(vaultPath, password, envName) {
384
+ const loadResult = await this.loadVault(vaultPath, password);
385
+ if (!loadResult.success) return loadResult;
386
+
387
+ try {
388
+ const vault = loadResult.data;
389
+ vault.removeEnvironment(envName);
390
+ return this.saveVault(vaultPath, password, vault);
391
+ } catch (error) {
392
+ return { success: false, error: error.message };
393
+ }
394
+ }
395
+
396
+ static async renameEnv(vaultPath, password, oldName, newName) {
397
+ const loadResult = await this.loadVault(vaultPath, password);
398
+ if (!loadResult.success) return loadResult;
399
+
400
+ try {
401
+ const vault = loadResult.data;
402
+ vault.renameEnvironment(oldName, newName);
403
+ return this.saveVault(vaultPath, password, vault);
404
+ } catch (error) {
405
+ return { success: false, error: error.message };
406
+ }
407
+ }
408
+
409
+ static async exportEnv(vaultPath, password, envName, format = 'dotenv') {
410
+ const loadResult = await this.loadVault(vaultPath, password);
411
+ if (!loadResult.success) return loadResult;
412
+
413
+ try {
414
+ const vault = loadResult.data;
415
+ const activeVersion = vault.getActiveVersion(envName);
416
+
417
+ if (!activeVersion) {
418
+ return {
419
+ success: false,
420
+ error: `Environment '${envName}' has no versions`,
421
+ };
422
+ }
423
+
424
+ if (format === 'json') {
425
+ return { success: true, data: activeVersion.vars };
426
+ }
427
+
428
+ const lines = Object.entries(activeVersion.vars).map(
429
+ ([key, value]) => `${key}=${value}`
430
+ );
431
+
432
+ return { success: true, data: lines.join('\n') + '\n' };
433
+ } catch (error) {
434
+ return { success: false, error: error.message };
435
+ }
436
+ }
437
+
438
+ static async templateEnv(vaultPath, password, envName) {
439
+ const loadResult = await this.loadVault(vaultPath, password);
440
+ if (!loadResult.success) return loadResult;
441
+
442
+ try {
443
+ const vault = loadResult.data;
444
+ const activeVersion = vault.getActiveVersion(envName);
445
+
446
+ if (!activeVersion) {
447
+ return {
448
+ success: false,
449
+ error: `Environment '${envName}' has no versions`,
450
+ };
451
+ }
452
+
453
+ const lines = Object.keys(activeVersion.vars).map((key) => {
454
+ if (activeVersion.required.includes(key)) {
455
+ return `${key}=<required>`;
456
+ }
457
+ return `${key}=`;
458
+ });
459
+
460
+ return { success: true, data: lines.join('\n') + '\n' };
461
+ } catch (error) {
462
+ return { success: false, error: error.message };
463
+ }
464
+ }
465
+
466
+ static async importEnvFile(vaultPath, password, envName, filePath) {
467
+ try {
468
+ const content = await fs.readFile(filePath, 'utf-8');
469
+ const loadResult = await this.loadVault(vaultPath, password);
470
+ if (!loadResult.success) return loadResult;
471
+
472
+ const vault = loadResult.data;
473
+ vault.importFromEnvFile(envName, content, {
474
+ message: `Imported from ${path.basename(filePath)}`,
475
+ });
476
+
477
+ return this.saveVault(vaultPath, password, vault);
478
+ } catch (error) {
479
+ return { success: false, error: error.message };
480
+ }
481
+ }
482
+
483
+ static async squashEnv(vaultPath, password, envName, keep = 1) {
484
+ const loadResult = await this.loadVault(vaultPath, password);
485
+ if (!loadResult.success) return loadResult;
486
+
487
+ try {
488
+ const vault = loadResult.data;
489
+ vault.squash(envName, { keep: Math.max(1, keep) });
490
+ return this.saveVault(vaultPath, password, vault);
491
+ } catch (error) {
492
+ return { success: false, error: error.message };
493
+ }
494
+ }
495
+
496
+ static async rollbackEnv(vaultPath, password, envName, versionN) {
497
+ const loadResult = await this.loadVault(vaultPath, password);
498
+ if (!loadResult.success) return loadResult;
499
+
500
+ try {
501
+ const vault = loadResult.data;
502
+ vault.rollback(envName, versionN);
503
+ return this.saveVault(vaultPath, password, vault);
504
+ } catch (error) {
505
+ return { success: false, error: error.message };
506
+ }
507
+ }
508
+
509
+ static async getHistory(vaultPath, password, envName) {
510
+ const loadResult = await this.loadVault(vaultPath, password);
511
+ if (!loadResult.success) return loadResult;
512
+
513
+ try {
514
+ const vault = loadResult.data;
515
+ const history = vault.getHistory(envName);
516
+ return { success: true, data: history };
517
+ } catch (error) {
518
+ return { success: false, error: error.message };
519
+ }
520
+ }
521
+
522
+ static async diffEnvs(vaultPath, password, envA, envB) {
523
+ const loadResult = await this.loadVault(vaultPath, password);
524
+ if (!loadResult.success) return loadResult;
525
+
526
+ try {
527
+ const vault = loadResult.data;
528
+ const versionA = vault.getActiveVersion(envA);
529
+ const versionB = vault.getActiveVersion(envB);
530
+
531
+ if (!versionA) {
532
+ return {
533
+ success: false,
534
+ error: `Environment '${envA}' has no versions`,
535
+ };
536
+ }
537
+ if (!versionB) {
538
+ return {
539
+ success: false,
540
+ error: `Environment '${envB}' has no versions`,
541
+ };
542
+ }
543
+
544
+ const keysA = new Set(Object.keys(versionA.vars));
545
+ const keysB = new Set(Object.keys(versionB.vars));
546
+
547
+ const added = [...keysB].filter((k) => !keysA.has(k));
548
+ const removed = [...keysA].filter((k) => !keysB.has(k));
549
+ const changed = [...keysA].filter(
550
+ (k) => keysB.has(k) && versionA.vars[k] !== versionB.vars[k]
551
+ );
552
+ const unchanged = [...keysA].filter(
553
+ (k) => keysB.has(k) && versionA.vars[k] === versionB.vars[k]
554
+ );
555
+
556
+ return {
557
+ success: true,
558
+ data: { added, removed, changed, unchanged },
559
+ };
560
+ } catch (error) {
561
+ return { success: false, error: error.message };
562
+ }
563
+ }
564
+ }
@@ -0,0 +1,126 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+
4
+ import { VaultFileService } from './VaultFileService.js';
5
+ import { CryptographyService } from './CryptographyService.js';
6
+ import path from 'path';
7
+
8
+ export class ImportExportService {
9
+ constructor(vaultDirectory) {
10
+ this.fileService = new VaultFileService(vaultDirectory);
11
+ }
12
+
13
+ _resolvePath(inputPath) {
14
+ if (inputPath.startsWith('~')) {
15
+ return path.join(os.homedir(), inputPath.slice(1));
16
+ }
17
+ return path.resolve(inputPath);
18
+ }
19
+
20
+ async exportVault(vaultName, password, exportPath) {
21
+ try {
22
+ if (!(await this.fileService.vaultExists(vaultName))) {
23
+ return { success: false, error: 'Vault not found' };
24
+ }
25
+
26
+ const vaultFile = await this.fileService.readVaultFile(vaultName);
27
+ const salt = Buffer.from(vaultFile.salt, 'hex');
28
+ const key = CryptographyService.deriveKey(password, salt);
29
+
30
+ try {
31
+ const encryptedData = {
32
+ encrypted: vaultFile.encrypted,
33
+ authTag: vaultFile.authTag,
34
+ iv: vaultFile.iv,
35
+ };
36
+ const decryptedData = CryptographyService.decrypt(encryptedData, key);
37
+
38
+ const exportData = {
39
+ exportVersion: '1.0',
40
+ exportedAt: new Date().toISOString(),
41
+ vaultName: vaultName,
42
+ originalVaultData: vaultFile,
43
+ metadata: {
44
+ version: decryptedData.version,
45
+ created: decryptedData.created,
46
+ entryCount: decryptedData.entries?.length || 0,
47
+ hasSettings: !!decryptedData.settings,
48
+ },
49
+ };
50
+
51
+ exportPath = this._resolvePath(exportPath);
52
+ await fs.writeJSON(exportPath, exportData, { spaces: 2 });
53
+ return { success: true };
54
+ } catch (decryptError) {
55
+ return { success: false, error: `Invalid password: ${decryptError}` };
56
+ }
57
+ } catch (error) {
58
+ console.error('Error exporting vault:', error);
59
+ return { success: false, error: 'Failed to export vault' };
60
+ }
61
+ }
62
+
63
+ async importVault(importPath, newVaultName, password) {
64
+ try {
65
+ if (await this.fileService.vaultExists(newVaultName)) {
66
+ return { success: false, error: 'Vault with this name already exists' };
67
+ }
68
+
69
+ importPath = this._resolvePath(importPath);
70
+ if (!(await fs.pathExists(importPath))) {
71
+ return { success: false, error: 'Import file not found' };
72
+ }
73
+
74
+ const importData = await fs.readJSON(importPath);
75
+
76
+ if (!importData.exportVersion || !importData.originalVaultData) {
77
+ return { success: false, error: 'Invalid import file format' };
78
+ }
79
+
80
+ const originalVaultData = importData.originalVaultData;
81
+ const originalSalt = Buffer.from(originalVaultData.salt, 'hex');
82
+
83
+ try {
84
+ const originalKey = CryptographyService.deriveKey(
85
+ password,
86
+ originalSalt
87
+ );
88
+ const originalEncryptedData = {
89
+ encrypted: originalVaultData.encrypted,
90
+ authTag: originalVaultData.authTag,
91
+ iv: originalVaultData.iv,
92
+ };
93
+ const decryptedData = CryptographyService.decrypt(
94
+ originalEncryptedData,
95
+ originalKey
96
+ );
97
+
98
+ // Re-encrypt with new salt for the imported vault
99
+ const newSalt = CryptographyService.generateSalt();
100
+ const newKey = CryptographyService.deriveKey(password, newSalt);
101
+ const newEncryptedData = CryptographyService.encrypt(
102
+ decryptedData,
103
+ newKey
104
+ );
105
+
106
+ const finalData = {
107
+ ...newEncryptedData,
108
+ salt: newSalt.toString('hex'),
109
+ recoveryMetadata: {}, // Imported vaults start without recovery metadata
110
+ };
111
+
112
+ await this.fileService.writeVaultFile(newVaultName, finalData);
113
+ return {
114
+ success: true,
115
+ metadata: importData.metadata,
116
+ importedAt: new Date().toISOString(),
117
+ };
118
+ } catch (decryptError) {
119
+ return { success: false, error: 'Invalid password for import file' };
120
+ }
121
+ } catch (error) {
122
+ console.error('Error importing vault:', error);
123
+ return { success: false, error: 'Failed to import vault' };
124
+ }
125
+ }
126
+ }