@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.
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/bin/cli.js +83 -0
- package/bin/commands/env.js +807 -0
- package/package.json +167 -0
- package/src/electron/models/EnvironmentVault.js +251 -0
- package/src/electron/models/Vault.js +87 -0
- package/src/electron/services/CryptographyService.js +54 -0
- package/src/electron/services/EnvironmentVaultService.js +564 -0
- package/src/electron/services/ImportExportService.js +126 -0
- package/src/electron/services/MenuService.js +110 -0
- package/src/electron/services/SecurityManager.js +109 -0
- package/src/electron/services/VaultFileService.js +137 -0
- package/src/electron/services/VaultRecoveryService.js +134 -0
- package/src/electron/services/VaultService.js +578 -0
- package/src/electron/services/VaultSettingsService.js +78 -0
- package/src/electron/services/WindowManager.js +266 -0
- package/src/electron/services/recovery/IRecoveryMethod.js +88 -0
- package/src/electron/services/recovery/KeyRecoveryService.js +245 -0
- package/src/electron/services/recovery/PasswordRecoveryService.js +128 -0
- package/src/electron/services/recovery/SecretQuestionRecoveryService.js +267 -0
- package/src/electron/services/recovery/UsbRecoveryService.js +244 -0
- package/src/electron/utils/appPaths.js +50 -0
- package/src/electron/utils/passwordValidation.js +29 -0
|
@@ -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
|
+
}
|