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