@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,128 @@
|
|
|
1
|
+
import { IRecoveryMethod, RecoveryData } from './IRecoveryMethod.js';
|
|
2
|
+
import { CryptographyService } from '../CryptographyService.js';
|
|
3
|
+
|
|
4
|
+
export class PasswordRecoveryService extends IRecoveryMethod {
|
|
5
|
+
#version = '1.0';
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.methodId = 'lastUsedPassword';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if the metadata is valid
|
|
14
|
+
* @param {string} vaultName
|
|
15
|
+
* @param {Object} metadata
|
|
16
|
+
* @returns {Promise<boolean>}
|
|
17
|
+
*/
|
|
18
|
+
isValid(vaultName, metadata) {
|
|
19
|
+
if (!!metadata && !metadata?.salt && !metadata?.encryptedMasterPassword) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the unique identifier for this recovery method
|
|
27
|
+
* @returns {string} Unique identifier
|
|
28
|
+
*/
|
|
29
|
+
getRecoveryMethodId() {
|
|
30
|
+
return this.methodId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate new recovery method
|
|
35
|
+
* @param {string} vaultName
|
|
36
|
+
* @returns {Promise<RecoveryData>} RecoveryData
|
|
37
|
+
*/
|
|
38
|
+
async generate(vaultName) {
|
|
39
|
+
// This is not useful for this method, return dummy result
|
|
40
|
+
return new RecoveryData({ data: { password: '' } });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create and returns metadata for the recovery option
|
|
45
|
+
* @param {string} vaultName
|
|
46
|
+
* @param {str} masterPassword
|
|
47
|
+
* @param {Object} recoveryData
|
|
48
|
+
* @returns {Promise<Object>} - metadata object
|
|
49
|
+
*/
|
|
50
|
+
createMetadata(vaultName, masterPassword, recoveryData = {}) {
|
|
51
|
+
// We just need to handle onPasswordChange event for this method
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recreate metadata when password change
|
|
57
|
+
* @param {string} vaultName
|
|
58
|
+
* @param {Object} metadata
|
|
59
|
+
* @param {str} oldPassword
|
|
60
|
+
* @param {str} newPassword
|
|
61
|
+
* @returns {Promise<Object>} - new metadata object
|
|
62
|
+
*/
|
|
63
|
+
onPasswordChange(vaultName, metadata, oldPassword, newPassword) {
|
|
64
|
+
try {
|
|
65
|
+
const salt = CryptographyService.generateSalt();
|
|
66
|
+
const oldPasswordKey = CryptographyService.deriveKey(oldPassword, salt);
|
|
67
|
+
const encryptedMasterPassword = CryptographyService.encrypt(
|
|
68
|
+
{ masterPassword: newPassword },
|
|
69
|
+
oldPasswordKey
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const newMetadata = {
|
|
73
|
+
salt: salt.toString('hex'),
|
|
74
|
+
encryptedMasterPassword,
|
|
75
|
+
createdAt: new Date().toISOString(),
|
|
76
|
+
version: this.#version,
|
|
77
|
+
};
|
|
78
|
+
return { success: true, metadata: newMetadata };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Failed to generate recovery metadata:', error);
|
|
81
|
+
return { success: false, error: 'Failed to generate recovery metadata!' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify recovery data
|
|
87
|
+
* @param {string} vaultName
|
|
88
|
+
* @param {Object} metadata
|
|
89
|
+
* @param {Object} recoveryData
|
|
90
|
+
* @returns {Promise<Object>} - Boolean indicating whether key is valid or not!
|
|
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<Object>} - Master password
|
|
111
|
+
*/
|
|
112
|
+
async recoverMasterPassword(vaultName, metadata, recoveryData = {}) {
|
|
113
|
+
try {
|
|
114
|
+
const salt = Buffer.from(metadata.salt, 'hex');
|
|
115
|
+
const oldPassword = recoveryData.data.password;
|
|
116
|
+
const oldPasswordKey = CryptographyService.deriveKey(oldPassword, salt);
|
|
117
|
+
const masterPassword = CryptographyService.decrypt(
|
|
118
|
+
metadata.encryptedMasterPassword,
|
|
119
|
+
oldPasswordKey
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return { success: true, ...masterPassword };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Invalid master password: ', error);
|
|
125
|
+
return { success: false, error: 'Invalid master password' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { IRecoveryMethod, RecoveryData } from './IRecoveryMethod.js';
|
|
2
|
+
import { CryptographyService } from '../CryptographyService.js';
|
|
3
|
+
|
|
4
|
+
export class SecretQuestionRecoveryService extends IRecoveryMethod {
|
|
5
|
+
#version = '1.0';
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.methodId = 'secretQuestions';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if the metadata is valid
|
|
14
|
+
* @param {string} vaultName
|
|
15
|
+
* @param {Object} metadata
|
|
16
|
+
* @returns {Promise<boolean>}
|
|
17
|
+
*/
|
|
18
|
+
isValid(vaultName, metadata) {
|
|
19
|
+
if (
|
|
20
|
+
!!metadata &&
|
|
21
|
+
!metadata?.salt &&
|
|
22
|
+
!metadata?.encryptedMasterPassword &&
|
|
23
|
+
!metadata?.encryptedRecoveryKey &&
|
|
24
|
+
!metadata?.encryptedQuestions
|
|
25
|
+
) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the unique identifier for this recovery method
|
|
33
|
+
* @returns {string} Unique identifier
|
|
34
|
+
*/
|
|
35
|
+
getRecoveryMethodId() {
|
|
36
|
+
return this.methodId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate new recovery method
|
|
41
|
+
* @param {string} vaultName
|
|
42
|
+
* @returns {Promise<RecoveryData>} RecoveryData
|
|
43
|
+
*/
|
|
44
|
+
async generate(vaultName) {
|
|
45
|
+
// This is not useful for this method, return dummy result
|
|
46
|
+
return new RecoveryData({ data: { questions: [] } });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create and returns metadata for the recovery option
|
|
51
|
+
* @param {string} vaultName
|
|
52
|
+
* @param {str} masterPassword
|
|
53
|
+
* @param {Object} recoveryData
|
|
54
|
+
* @returns {Promise<Object>} - metadata object
|
|
55
|
+
*/
|
|
56
|
+
createMetadata(vaultName, masterPassword, recoveryData = {}) {
|
|
57
|
+
if (
|
|
58
|
+
!recoveryData.data.questions ||
|
|
59
|
+
recoveryData.data.questions.length === 0
|
|
60
|
+
) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const salt = CryptographyService.generateSalt();
|
|
65
|
+
// The recovery key will be used to encrypt the master password
|
|
66
|
+
// and it it will be encrypted using the master password and questions
|
|
67
|
+
const recoveryKey = this.#generateRecoveryKey();
|
|
68
|
+
const masterKey = CryptographyService.deriveKey(masterPassword, salt);
|
|
69
|
+
const recoveryKeyDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
|
|
70
|
+
recoveryKey,
|
|
71
|
+
salt
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const encryptedRecoveryKey = CryptographyService.encrypt(
|
|
75
|
+
{ recoveryKey },
|
|
76
|
+
masterKey
|
|
77
|
+
);
|
|
78
|
+
const encryptedMasterPassword = CryptographyService.encrypt(
|
|
79
|
+
{ masterPassword },
|
|
80
|
+
recoveryKeyDerived
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Encrypt questions using the recovery key
|
|
84
|
+
encryptedQuestions = [];
|
|
85
|
+
for (const question of recoveryData.data.questions) {
|
|
86
|
+
const answerKeyDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
|
|
87
|
+
question.answer,
|
|
88
|
+
salt
|
|
89
|
+
);
|
|
90
|
+
const encryptedRecoveryKey = CryptographyService.encrypt(
|
|
91
|
+
{ recoveryKey },
|
|
92
|
+
answerKeyDerived
|
|
93
|
+
);
|
|
94
|
+
const encryptedAnswer = CryptographyService.encrypt(
|
|
95
|
+
{ answer: question.answer },
|
|
96
|
+
recoveryKeyDerived
|
|
97
|
+
);
|
|
98
|
+
const questionHash = CryptographyService.hashPassword(question.question);
|
|
99
|
+
encryptedQuestions.push({
|
|
100
|
+
questionHash,
|
|
101
|
+
encryptedQuestion: CryptographyService.encrypt(
|
|
102
|
+
question,
|
|
103
|
+
encryptedAnswer,
|
|
104
|
+
encryptedRecoveryKey
|
|
105
|
+
),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const createdAt = new Date().toISOString();
|
|
110
|
+
return {
|
|
111
|
+
salt: salt.toString('hex'),
|
|
112
|
+
encryptedRecoveryKey,
|
|
113
|
+
encryptedMasterPassword,
|
|
114
|
+
encryptedQuestions,
|
|
115
|
+
createdAt: createdAt,
|
|
116
|
+
updatedAt: createdAt,
|
|
117
|
+
version: this.#version,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Recreate metadata when password change
|
|
123
|
+
* @param {string} vaultName
|
|
124
|
+
* @param {Object} metadata
|
|
125
|
+
* @param {str} oldPassword
|
|
126
|
+
* @param {str} newPassword
|
|
127
|
+
* @returns {Promise<Object>} - new metadata object
|
|
128
|
+
*/
|
|
129
|
+
onPasswordChange(vaultName, metadata, oldPassword, newPassword) {
|
|
130
|
+
try {
|
|
131
|
+
const salt = Buffer.from(metadata.salt, 'hex');
|
|
132
|
+
const oldKey = CryptographyService.deriveKey(oldPassword, salt);
|
|
133
|
+
const recoveryKey = CryptographyService.decrypt(
|
|
134
|
+
metadata.encryptedRecoveryKey,
|
|
135
|
+
oldKey
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Generate new metadata
|
|
139
|
+
const masterKey = CryptographyService.deriveKey(newPassword, salt);
|
|
140
|
+
const recoveryKeyDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
|
|
141
|
+
recoveryKey,
|
|
142
|
+
salt
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const encryptedRecoveryKey = CryptographyService.encrypt(
|
|
146
|
+
{ recoveryKey },
|
|
147
|
+
masterKey
|
|
148
|
+
);
|
|
149
|
+
const encryptedMasterPassword = CryptographyService.encrypt(
|
|
150
|
+
{ masterPassword: newPassword },
|
|
151
|
+
recoveryKeyDerived
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const newMetadata = {
|
|
155
|
+
...metadata,
|
|
156
|
+
encryptedMasterPassword,
|
|
157
|
+
encryptedRecoveryKey,
|
|
158
|
+
updatedAt: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
return { success: true, metadata: newMetadata };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(
|
|
163
|
+
'Internal error: Look like the metadata is corrupted:',
|
|
164
|
+
error
|
|
165
|
+
);
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: 'Internal error: Look like the metadata is corrupted.',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Verify recovery data
|
|
175
|
+
* @param {string} vaultName
|
|
176
|
+
* @param {Object} metadata
|
|
177
|
+
* @param {Object} recoveryData
|
|
178
|
+
* @returns {Promise<Object>} - Boolean indicating whether key is valid or not!
|
|
179
|
+
*/
|
|
180
|
+
async verify(vaultName, metadata = {}, recoveryData = {}) {
|
|
181
|
+
const result = await this.recoverMasterPassword(
|
|
182
|
+
vaultName,
|
|
183
|
+
metadata,
|
|
184
|
+
recoveryData
|
|
185
|
+
);
|
|
186
|
+
if (result.success) {
|
|
187
|
+
return { success: true };
|
|
188
|
+
} else {
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* recover master password
|
|
195
|
+
* @param {string} vaultName
|
|
196
|
+
* @param {Object} metadata
|
|
197
|
+
* @param {Object} recoveryData
|
|
198
|
+
* @returns {Promise<Object>} - Master password
|
|
199
|
+
*/
|
|
200
|
+
async recoverMasterPassword(vaultName, metadata, recoveryData = {}) {
|
|
201
|
+
try {
|
|
202
|
+
const salt = Buffer.from(metadata.salt, 'hex');
|
|
203
|
+
|
|
204
|
+
for (const question in recoveryData.data.questions) {
|
|
205
|
+
const questionHash = CryptographyService.hashPassword(
|
|
206
|
+
question.question
|
|
207
|
+
);
|
|
208
|
+
for (const storedQuestion of metadata.encryptedQuestions) {
|
|
209
|
+
if (questionHash !== storedQuestion.questionHash) {
|
|
210
|
+
try {
|
|
211
|
+
const answerDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
|
|
212
|
+
question.answer,
|
|
213
|
+
salt
|
|
214
|
+
);
|
|
215
|
+
const decryptedRecoveryKey = CryptographyService.decrypt(
|
|
216
|
+
storedQuestion.encryptedQuestion,
|
|
217
|
+
answerDerived
|
|
218
|
+
);
|
|
219
|
+
const recoveryKey = decryptedRecoveryKey.recoveryKey;
|
|
220
|
+
const recoveryKeyDerived =
|
|
221
|
+
KeyRecoveryService.deriveKeyFromRecoveryKey(recoveryKey, salt);
|
|
222
|
+
const masterPassword = CryptographyService.decrypt(
|
|
223
|
+
metadata.encryptedMasterPassword,
|
|
224
|
+
recoveryKeyDerived
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return { success: true, ...masterPassword };
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('Invalid question:', error);
|
|
230
|
+
return { success: false, error: `Invalid question: ${error}` };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { success: true, ...masterPassword };
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Invalid question: ', error);
|
|
239
|
+
return { success: false, error: 'Invalid question' };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#generateRecoveryKey() {
|
|
244
|
+
const recoveryKeyBytes = crypto.randomBytes(32);
|
|
245
|
+
|
|
246
|
+
let result = '';
|
|
247
|
+
let bits = 0;
|
|
248
|
+
let value = 0;
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < recoveryKeyBytes.length; i++) {
|
|
251
|
+
value = (value << 8) | recoveryKeyBytes[i];
|
|
252
|
+
bits += 8;
|
|
253
|
+
|
|
254
|
+
while (bits >= 5) {
|
|
255
|
+
result +=
|
|
256
|
+
KeyRecoveryService.BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
|
257
|
+
bits -= 5;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (bits > 0) {
|
|
262
|
+
result += KeyRecoveryService.BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result.match(/.{1,4}/g).join('-');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
import { IRecoveryMethod, RecoveryData } from './IRecoveryMethod.js';
|
|
4
|
+
import { CryptographyService } from '../CryptographyService.js';
|
|
5
|
+
|
|
6
|
+
export class UsbRecoveryService extends IRecoveryMethod {
|
|
7
|
+
#version = '1.0';
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.methodId = 'usbDrive';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the unique identifier for this recovery method
|
|
16
|
+
* @returns {string} Unique identifier
|
|
17
|
+
*/
|
|
18
|
+
getRecoveryMethodId() {
|
|
19
|
+
return this.methodId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if the metadata is valid
|
|
24
|
+
* @param {string} vaultName
|
|
25
|
+
* @param {Object} metadata
|
|
26
|
+
* @returns {Promise<boolean>}
|
|
27
|
+
*/
|
|
28
|
+
isValid(vaultName, metadata) {
|
|
29
|
+
if (
|
|
30
|
+
!!!metadata ||
|
|
31
|
+
!metadata?.salt ||
|
|
32
|
+
!metadata?.encryptedMasterPassword ||
|
|
33
|
+
!metadata?.encryptedRecoveryKey
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate new recovery method
|
|
42
|
+
* @param {string} vaultName
|
|
43
|
+
* @returns {Promise<RecoveryData>} RecoveryData
|
|
44
|
+
*/
|
|
45
|
+
async generate(vaultName) {
|
|
46
|
+
// This is not useful for this method, return dummy result
|
|
47
|
+
return new RecoveryData({ data: { usbDrives: [] } });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create and returns metadata for the recovery option
|
|
52
|
+
* @param {string} vaultName
|
|
53
|
+
* @param {str} masterPassword
|
|
54
|
+
* @param {Object} recoveryData
|
|
55
|
+
* @returns {Promise<Object>} - metadata object
|
|
56
|
+
*/
|
|
57
|
+
createMetadata(vaultName, masterPassword, recoveryData = {}) {
|
|
58
|
+
const salt = CryptographyService.generateSalt();
|
|
59
|
+
const recoveryKey = recoveryData.data.key;
|
|
60
|
+
const masterKey = CryptographyService.deriveKey(masterPassword, salt);
|
|
61
|
+
const recoveryKeyDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
|
|
62
|
+
recoveryKey,
|
|
63
|
+
salt
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const encryptedRecoveryKey = CryptographyService.encrypt(
|
|
67
|
+
{ recoveryKey },
|
|
68
|
+
masterKey
|
|
69
|
+
);
|
|
70
|
+
const encryptedMasterPassword = CryptographyService.encrypt(
|
|
71
|
+
{ masterPassword },
|
|
72
|
+
recoveryKeyDerived
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
salt: salt.toString('hex'),
|
|
77
|
+
encryptedRecoveryKey,
|
|
78
|
+
encryptedMasterPassword,
|
|
79
|
+
createdAt: new Date().toISOString(),
|
|
80
|
+
version: this.#version,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Verify recovery data
|
|
86
|
+
* @param {string} vaultName
|
|
87
|
+
* @param {Object} metadata
|
|
88
|
+
* @param {Object} recoveryData
|
|
89
|
+
* @returns {Promise<object>} - Result
|
|
90
|
+
*/
|
|
91
|
+
async verify(vaultName, metadata, recoveryData) {
|
|
92
|
+
const result = await this.recoverMasterPassword(
|
|
93
|
+
vaultName,
|
|
94
|
+
metadata,
|
|
95
|
+
recoveryData
|
|
96
|
+
);
|
|
97
|
+
if (result.success) {
|
|
98
|
+
return { success: true };
|
|
99
|
+
} else {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* recover master password
|
|
106
|
+
* @param {string} vaultName
|
|
107
|
+
* @param {Object} metadata
|
|
108
|
+
* @param {Object} recoveryData
|
|
109
|
+
* @returns {Promise<str>} - Master password
|
|
110
|
+
*/
|
|
111
|
+
async recoverMasterPassword(vaultName, metadata, recoveryData) {
|
|
112
|
+
// TODO: validate metadata/recoveryData
|
|
113
|
+
try {
|
|
114
|
+
const recoveryKey = recoveryData.data.key;
|
|
115
|
+
if (!this.#validateRecoveryKeyFormat(recoveryKey)) {
|
|
116
|
+
return { success: false, error: 'Invalid recovery key format' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const salt = Buffer.from(metadata.salt, 'hex');
|
|
120
|
+
const recoveryKeyDerived = KeyRecoveryService.deriveKeyFromRecoveryKey(
|
|
121
|
+
recoveryKey,
|
|
122
|
+
salt
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const masterPassword = CryptographyService.decrypt(
|
|
127
|
+
metadata.encryptedMasterPassword,
|
|
128
|
+
recoveryKeyDerived
|
|
129
|
+
);
|
|
130
|
+
return { success: true, ...masterPassword };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Invalid recovery key:', error);
|
|
133
|
+
return { success: false, error: `Invalid recovery key: ${error}` };
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('Failed to recover vault using given recovery key:', error);
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: 'Failed to recover vault using given recovery key!',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Recreate metadata when password change
|
|
146
|
+
* @param {string} vaultName
|
|
147
|
+
* @param {Object} metadata
|
|
148
|
+
* @param {str} oldPassword
|
|
149
|
+
* @param {str} newPassword
|
|
150
|
+
* @returns {Promise<Object>} - new metadata object
|
|
151
|
+
*/
|
|
152
|
+
onPasswordChange(vaultName, metadata, oldPassword, newPassword) {
|
|
153
|
+
const result = this.#loadRecoveryKey(vaultName, metadata, oldPassword);
|
|
154
|
+
if (!result.success) {
|
|
155
|
+
console.warn(`Unable to load recovery key for vault: ${vaultName}.`);
|
|
156
|
+
console.log(
|
|
157
|
+
'Looks like the recovery key does not exist or already corrupted!'
|
|
158
|
+
);
|
|
159
|
+
return { success: false, metadata };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const recoveryData = new RecoveryData({
|
|
164
|
+
data: { key: result.recoveryKey },
|
|
165
|
+
});
|
|
166
|
+
const newMetadata = this.createMetadata(
|
|
167
|
+
vaultName,
|
|
168
|
+
newPassword,
|
|
169
|
+
recoveryData
|
|
170
|
+
);
|
|
171
|
+
return { success: true, metadata: newMetadata };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Failed to create metadata:', error);
|
|
174
|
+
return { success: false, error: 'Failed to create metadata' };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#loadRecoveryKey(vaultName, metadata, masterPassword) {
|
|
179
|
+
// TODO: validate metadata
|
|
180
|
+
try {
|
|
181
|
+
const salt = Buffer.from(metadata.salt, 'hex');
|
|
182
|
+
const key = CryptographyService.deriveKey(masterPassword, salt);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const recoveryKey = CryptographyService.decrypt(
|
|
186
|
+
metadata.encryptedRecoveryKey,
|
|
187
|
+
key
|
|
188
|
+
);
|
|
189
|
+
return { success: true, ...recoveryKey };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Invalid recovery key:', error);
|
|
192
|
+
return { success: false, error: `Invalid recovery key: ${error}` };
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(
|
|
196
|
+
'Internal error: Look like the recovery key is corrupted:',
|
|
197
|
+
error
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
error: 'Internal error: Look like the recovery key is corrupted.',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#generateRecoveryKey() {
|
|
207
|
+
const recoveryKeyBytes = crypto.randomBytes(32);
|
|
208
|
+
|
|
209
|
+
let result = '';
|
|
210
|
+
let bits = 0;
|
|
211
|
+
let value = 0;
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < recoveryKeyBytes.length; i++) {
|
|
214
|
+
value = (value << 8) | recoveryKeyBytes[i];
|
|
215
|
+
bits += 8;
|
|
216
|
+
|
|
217
|
+
while (bits >= 5) {
|
|
218
|
+
result +=
|
|
219
|
+
KeyRecoveryService.BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
|
220
|
+
bits -= 5;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (bits > 0) {
|
|
225
|
+
result += KeyRecoveryService.BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result.match(/.{1,4}/g).join('-');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#validateRecoveryKeyFormat(recoveryKey) {
|
|
232
|
+
const cleanKey = recoveryKey.replace(/-/g, '').toUpperCase();
|
|
233
|
+
const base32Regex = /^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/;
|
|
234
|
+
return (
|
|
235
|
+
base32Regex.test(cleanKey) &&
|
|
236
|
+
cleanKey.length >= KeyRecoveryService.MIN_KEY_LENGTH
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static deriveKeyFromRecoveryKey(recoveryKey, salt) {
|
|
241
|
+
const cleanKey = recoveryKey.replace(/-/g, '').toUpperCase();
|
|
242
|
+
return CryptographyService.deriveKey(cleanKey, salt);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Single source of truth for where SecureVault stores its data on disk.
|
|
6
|
+
*
|
|
7
|
+
* Both the desktop app (Electron main process) and the `vault` CLI must
|
|
8
|
+
* resolve to the *same* directory so that vaults created in one are visible
|
|
9
|
+
* in the other. We deliberately do NOT derive this from Electron's
|
|
10
|
+
* `app.getPath('userData')` (which is based on the packaged productName
|
|
11
|
+
* "Secure Password Manager") nor from package.json `name` — those diverge
|
|
12
|
+
* between dev/packaged builds and the CLI. Keep this constant stable across
|
|
13
|
+
* renames; changing it orphans existing vaults.
|
|
14
|
+
*/
|
|
15
|
+
export const APP_DATA_DIR_NAME = 'secure-password-manager';
|
|
16
|
+
|
|
17
|
+
function resolvePath(...segments) {
|
|
18
|
+
return process.platform === 'win32'
|
|
19
|
+
? path.win32.join(...segments)
|
|
20
|
+
: path.posix.join(...segments);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Absolute path to the application data directory for the current platform. */
|
|
24
|
+
export function getAppDataPath() {
|
|
25
|
+
switch (process.platform) {
|
|
26
|
+
case 'darwin':
|
|
27
|
+
return resolvePath(
|
|
28
|
+
os.homedir(),
|
|
29
|
+
'Library',
|
|
30
|
+
'Application Support',
|
|
31
|
+
APP_DATA_DIR_NAME
|
|
32
|
+
);
|
|
33
|
+
case 'win32':
|
|
34
|
+
return resolvePath(process.env.APPDATA, APP_DATA_DIR_NAME);
|
|
35
|
+
case 'linux':
|
|
36
|
+
return resolvePath(os.homedir(), `.${APP_DATA_DIR_NAME}`);
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Directory holding the regular password vaults. */
|
|
43
|
+
export function getVaultsDir() {
|
|
44
|
+
return resolvePath(getAppDataPath(), 'vaults');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Directory holding the environment (`.env`) vaults. */
|
|
48
|
+
export function getEnvsDir() {
|
|
49
|
+
return resolvePath(getAppDataPath(), 'envs');
|
|
50
|
+
}
|