@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,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
|
+
}
|