@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/package.json ADDED
@@ -0,0 +1,167 @@
1
+ {
2
+ "name": "@benzid.wael/secure-vault",
3
+ "version": "0.0.1",
4
+ "description": "A secure password management application built with Electron and React",
5
+ "type": "module",
6
+ "main": "build/electron/main.cjs",
7
+ "homepage": "./",
8
+ "license": "MIT",
9
+ "author": {
10
+ "name": "Wael Ben Zid",
11
+ "email": "benzid.wael@hotmail.fr"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/benzid-wael/secure-vault.git"
16
+ },
17
+ "engines": {
18
+ "node": ">=20.10.0"
19
+ },
20
+ "files": [
21
+ "bin/",
22
+ "src/electron/",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "dev": "vite",
28
+ "start": "vite",
29
+ "build": "vite build",
30
+ "preview": "vite preview",
31
+ "test": "vitest",
32
+ "test:ui": "vitest --ui",
33
+ "test:coverage": "vitest run --coverage",
34
+ "test:coverage:ui": "vitest --ui --coverage",
35
+ "test:coverage:check": "node scripts/coverage-check.js",
36
+ "dev:electron": "npm run build:main && concurrently -k -n VITE,ELECTRON -c blue,green \"vite\" \"wait-on tcp:3000 && cross-env ELECTRON_IS_DEV=1 electron .\"",
37
+ "dev:renderer": "vite",
38
+ "dev:main": "cross-env NODE_ENV=development electron .",
39
+ "build:renderer": "vite build",
40
+ "build:main": "vite build --config vite.electron.config.js",
41
+ "build:all": "npm run build:renderer && npm run build:main",
42
+ "verify-build": "node scripts/verify-build.js",
43
+ "start:dev": "cross-env NODE_ENV=development electron .",
44
+ "start:prod": "cross-env NODE_ENV=production electron .",
45
+ "package": "npm run build:all && electron-builder",
46
+ "package:mac": "npm run build:all && electron-builder --mac",
47
+ "package:win": "npm run build:all && electron-builder --win",
48
+ "package:linux": "npm run build:all && electron-builder --linux",
49
+ "package:cli": "node scripts/build-cli.js",
50
+ "electron": "npm run start:prod",
51
+ "electron-dev": "npm run dev:electron",
52
+ "electron:build": "npm run build:all",
53
+ "electron-pack": "npm run package",
54
+ "preelectron-pack": "npm run build:all",
55
+ "prepare": "husky"
56
+ },
57
+ "dependencies": {
58
+ "@emotion/react": "^11.11.1",
59
+ "@emotion/styled": "^11.11.0",
60
+ "@inquirer/password": "^4.0.17",
61
+ "@mui/icons-material": "^5.14.1",
62
+ "@mui/material": "^5.14.1",
63
+ "chalk": "^5.6.0",
64
+ "commander": "^14.0.0",
65
+ "crypto-js": "^4.1.1",
66
+ "electron-is-dev": "^2.0.0",
67
+ "figlet": "^1.8.2",
68
+ "fs-extra": "^11.1.1",
69
+ "node-forge": "^1.3.1",
70
+ "ora": "^8.2.0",
71
+ "path": "^0.12.7",
72
+ "react": "^18.3.1",
73
+ "react-dom": "^18.3.1",
74
+ "react-router-dom": "^6.14.2",
75
+ "uuid": "^9.0.0"
76
+ },
77
+ "devDependencies": {
78
+ "@babel/core": "^7.23.5",
79
+ "@emotion/babel-plugin": "^11.11.0",
80
+ "@testing-library/jest-dom": "^6.1.4",
81
+ "@testing-library/react": "^14.1.2",
82
+ "@testing-library/user-event": "^14.5.1",
83
+ "@types/node": "^20.10.5",
84
+ "@types/react": "^18.2.45",
85
+ "@types/react-dom": "^18.2.18",
86
+ "@vitejs/plugin-react": "^4.2.1",
87
+ "@vitest/coverage-v8": "^1.2.2",
88
+ "@vitest/ui": "^1.2.2",
89
+ "concurrently": "^8.2.2",
90
+ "cross-env": "^7.0.3",
91
+ "electron": "^27.1.3",
92
+ "electron-builder": "^24.6.4",
93
+ "eslint": "^8.56.0",
94
+ "eslint-plugin-react": "^7.33.2",
95
+ "eslint-plugin-react-hooks": "^4.6.0",
96
+ "eslint-plugin-react-refresh": "^0.4.5",
97
+ "husky": "^9.1.7",
98
+ "jsdom": "^22.1.0",
99
+ "lint-staged": "^15.2.0",
100
+ "prettier": "^3.1.1",
101
+ "typescript": "^5.3.2",
102
+ "vite": "^5.0.8",
103
+ "vite-plugin-electron": "^0.15.5",
104
+ "vitest": "^1.2.2",
105
+ "wait-on": "^7.2.0"
106
+ },
107
+ "lint-staged": {
108
+ "*.{js,jsx,ts,tsx}": [
109
+ "npx prettier --write"
110
+ ],
111
+ "*.{json,md,css}": [
112
+ "npx prettier --write"
113
+ ]
114
+ },
115
+ "build": {
116
+ "appId": "com.securepasswordmanager.app",
117
+ "productName": "Secure Password Manager",
118
+ "files": [
119
+ "build/**/*",
120
+ "node_modules/**/*"
121
+ ],
122
+ "directories": {
123
+ "buildResources": "public",
124
+ "output": "dist"
125
+ },
126
+ "publish": null,
127
+ "mac": {
128
+ "category": "public.app-category.utilities",
129
+ "target": [
130
+ "dmg",
131
+ "zip"
132
+ ]
133
+ },
134
+ "win": {
135
+ "target": [
136
+ "nsis",
137
+ "portable"
138
+ ]
139
+ },
140
+ "linux": {
141
+ "target": [
142
+ "AppImage",
143
+ "deb"
144
+ ],
145
+ "category": "Utility"
146
+ },
147
+ "nsis": {
148
+ "oneClick": false,
149
+ "allowToChangeInstallationDirectory": true
150
+ }
151
+ },
152
+ "browserslist": {
153
+ "production": [
154
+ ">0.2%",
155
+ "not dead",
156
+ "not op_mini all"
157
+ ],
158
+ "development": [
159
+ "last 1 chrome version",
160
+ "last 1 firefox version",
161
+ "last 1 safari version"
162
+ ]
163
+ },
164
+ "bin": {
165
+ "vault": "./bin/cli.js"
166
+ }
167
+ }
@@ -0,0 +1,251 @@
1
+ export class EnvironmentVault {
2
+ constructor({
3
+ vaultVersion = 1,
4
+ created = new Date().toISOString(),
5
+ updated = new Date().toISOString(),
6
+ environments = {},
7
+ } = {}) {
8
+ this.vaultVersion = vaultVersion;
9
+ this.created = created;
10
+ this.updated = updated;
11
+ this.environments = {};
12
+
13
+ for (const [name, env] of Object.entries(environments)) {
14
+ this.environments[name] = {
15
+ description: env.description || '',
16
+ versions: (env.versions || []).map((v) => ({ ...v })),
17
+ activeVersion: env.activeVersion || 1,
18
+ extends: env.extends || null,
19
+ };
20
+ }
21
+ }
22
+
23
+ toJSON() {
24
+ return {
25
+ vaultVersion: this.vaultVersion,
26
+ created: this.created,
27
+ updated: this.updated,
28
+ environments: Object.fromEntries(
29
+ Object.entries(this.environments).map(([name, env]) => [
30
+ name,
31
+ {
32
+ description: env.description,
33
+ versions: env.versions.map((v) => ({ ...v })),
34
+ activeVersion: env.activeVersion,
35
+ extends: env.extends,
36
+ },
37
+ ])
38
+ ),
39
+ };
40
+ }
41
+
42
+ static fromJSON(data) {
43
+ return new EnvironmentVault(data);
44
+ }
45
+
46
+ listEnvironmentNames() {
47
+ return Object.keys(this.environments).sort();
48
+ }
49
+
50
+ addEnvironment(name, { description = '' } = {}) {
51
+ if (this.environments[name]) {
52
+ throw new Error(`Environment '${name}' already exists`);
53
+ }
54
+ this.environments[name] = {
55
+ description,
56
+ versions: [],
57
+ activeVersion: 0,
58
+ extends: null,
59
+ };
60
+ this.updated = new Date().toISOString();
61
+ }
62
+
63
+ removeEnvironment(name) {
64
+ if (!this.environments[name]) {
65
+ throw new Error(`Environment '${name}' not found`);
66
+ }
67
+ delete this.environments[name];
68
+ this.updated = new Date().toISOString();
69
+ }
70
+
71
+ renameEnvironment(oldName, newName) {
72
+ if (!this.environments[oldName]) {
73
+ throw new Error(`Environment '${oldName}' not found`);
74
+ }
75
+ if (this.environments[newName]) {
76
+ throw new Error(`Environment '${newName}' already exists`);
77
+ }
78
+ if (oldName === newName) return;
79
+
80
+ this.environments[newName] = this.environments[oldName];
81
+ delete this.environments[oldName];
82
+ this.updated = new Date().toISOString();
83
+ }
84
+
85
+ #getEnv(name) {
86
+ const env = this.environments[name];
87
+ if (!env) {
88
+ throw new Error(`Environment '${name}' not found`);
89
+ }
90
+ return env;
91
+ }
92
+
93
+ #getVersion(env, versionN) {
94
+ const version = env.versions.find((v) => v.n === versionN);
95
+ if (!version) {
96
+ throw new Error(`Version ${versionN} not found in environment`);
97
+ }
98
+ return version;
99
+ }
100
+
101
+ addVersion(
102
+ name,
103
+ vars,
104
+ { required = [], nonSensitive = [], message = null } = {}
105
+ ) {
106
+ const env = this.#getEnv(name);
107
+ const nextN =
108
+ env.versions.length > 0 ? env.versions[env.versions.length - 1].n + 1 : 1;
109
+
110
+ const version = {
111
+ n: nextN,
112
+ created: new Date().toISOString(),
113
+ message: message,
114
+ vars: { ...vars },
115
+ required: [...required],
116
+ nonSensitive: [...nonSensitive],
117
+ };
118
+
119
+ env.versions.push(version);
120
+ env.activeVersion = nextN;
121
+ this.updated = new Date().toISOString();
122
+ return version;
123
+ }
124
+
125
+ getActiveVersion(name) {
126
+ const env = this.#getEnv(name);
127
+ if (env.activeVersion === 0 || env.versions.length === 0) {
128
+ return null;
129
+ }
130
+ return this.#getVersion(env, env.activeVersion);
131
+ }
132
+
133
+ setActiveVersion(name, versionN) {
134
+ const env = this.#getEnv(name);
135
+ this.#getVersion(env, versionN);
136
+ env.activeVersion = versionN;
137
+ this.updated = new Date().toISOString();
138
+ }
139
+
140
+ getVersion(name, versionN) {
141
+ const env = this.#getEnv(name);
142
+ return JSON.parse(JSON.stringify(this.#getVersion(env, versionN)));
143
+ }
144
+
145
+ getHistory(name) {
146
+ const env = this.#getEnv(name);
147
+ return env.versions.map((v) => ({
148
+ n: v.n,
149
+ created: v.created,
150
+ message: v.message,
151
+ keyCount: Object.keys(v.vars).length,
152
+ isActive: v.n === env.activeVersion,
153
+ }));
154
+ }
155
+
156
+ rollback(name, versionN) {
157
+ const env = this.#getEnv(name);
158
+ const source = this.#getVersion(env, versionN);
159
+
160
+ return this.addVersion(
161
+ name,
162
+ { ...source.vars },
163
+ {
164
+ required: [...source.required],
165
+ nonSensitive: [...source.nonSensitive],
166
+ message: `Rollback to version ${versionN}`,
167
+ }
168
+ );
169
+ }
170
+
171
+ squash(name, { keep = 1 } = {}) {
172
+ const env = this.#getEnv(name);
173
+ const keepN = Math.max(1, keep);
174
+ if (env.versions.length <= keepN) return;
175
+
176
+ const keepFromEnd = keepN - 1;
177
+ const versionsToKeep =
178
+ keepFromEnd > 0 ? env.versions.slice(-keepFromEnd) : [];
179
+ const versionsToSquash =
180
+ keepFromEnd > 0 ? env.versions.slice(0, -keepFromEnd) : [...env.versions];
181
+
182
+ const lastSquashed = versionsToSquash[versionsToSquash.length - 1];
183
+ const squashedVars = { ...lastSquashed.vars };
184
+ const squashedRequired = [...lastSquashed.required];
185
+ const squashedNonSensitive = [...lastSquashed.nonSensitive];
186
+
187
+ const wasActiveSquashed = env.activeVersion <= lastSquashed.n;
188
+
189
+ env.versions = [
190
+ {
191
+ n: 1,
192
+ created: versionsToSquash[0].created,
193
+ message: `Squashed ${versionsToSquash.length} versions (v${versionsToSquash[0].n}–v${lastSquashed.n})`,
194
+ vars: squashedVars,
195
+ required: squashedRequired,
196
+ nonSensitive: squashedNonSensitive,
197
+ },
198
+ ...versionsToKeep.map((v, i) => ({
199
+ ...v,
200
+ n: i + 2,
201
+ })),
202
+ ];
203
+
204
+ env.activeVersion = wasActiveSquashed ? 1 : env.versions.length;
205
+ this.updated = new Date().toISOString();
206
+ }
207
+
208
+ isSensitive(name, key) {
209
+ const version = this.getActiveVersion(name);
210
+ if (!version) return true;
211
+ return !version.nonSensitive.includes(key);
212
+ }
213
+
214
+ static parseEnvFile(content) {
215
+ const vars = {};
216
+ const lines = content.split('\n');
217
+
218
+ for (const rawLine of lines) {
219
+ const line = rawLine.trim();
220
+
221
+ if (!line || line.startsWith('#')) continue;
222
+
223
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
224
+ if (!match) continue;
225
+
226
+ const key = match[1];
227
+ let value = match[2];
228
+
229
+ if (
230
+ (value.startsWith('"') && value.endsWith('"')) ||
231
+ (value.startsWith("'") && value.endsWith("'"))
232
+ ) {
233
+ value = value.slice(1, -1);
234
+ }
235
+
236
+ vars[key] = value;
237
+ }
238
+
239
+ return vars;
240
+ }
241
+
242
+ importFromEnvFile(name, content, { message = null } = {}) {
243
+ const vars = EnvironmentVault.parseEnvFile(content);
244
+
245
+ if (!this.environments[name]) {
246
+ this.addEnvironment(name);
247
+ }
248
+
249
+ return this.addVersion(name, vars, { message });
250
+ }
251
+ }
@@ -0,0 +1,87 @@
1
+ export class Vault {
2
+ constructor({
3
+ name,
4
+ version = '1.0',
5
+ created = new Date().toISOString(),
6
+ lastPasswordChange = new Date().toISOString(),
7
+ entries = {},
8
+ passwordHistory = [],
9
+ settings = {},
10
+ isDefault = false,
11
+ } = {}) {
12
+ this.name = name;
13
+ this.version = version;
14
+ this.created = created;
15
+ this.lastPasswordChange = lastPasswordChange;
16
+ this.entries = entries;
17
+ this.passwordHistory = passwordHistory;
18
+ this.settings = this._validateSettings(settings);
19
+ this.isDefault = isDefault;
20
+ }
21
+
22
+ _validateSettings(settings) {
23
+ return {
24
+ enforcePasswordChange: settings.enforcePasswordChange ?? false,
25
+ passwordChangeWarningDays: Math.max(
26
+ 1,
27
+ settings.passwordChangeWarningDays ?? 90
28
+ ),
29
+ preventPasswordReuse: settings.preventPasswordReuse ?? true,
30
+ maxPasswordHistory: Math.max(1, settings.maxPasswordHistory ?? 3),
31
+ ...settings,
32
+ };
33
+ }
34
+
35
+ updateSettings(newSettings) {
36
+ this.settings = this._validateSettings({
37
+ ...this.settings,
38
+ ...newSettings,
39
+ });
40
+ }
41
+
42
+ addPasswordToHistory(passwordHash) {
43
+ this.passwordHistory.unshift({
44
+ changedAt: this.lastPasswordChange,
45
+ passwordHash,
46
+ });
47
+
48
+ // Keep only the specified number of password history entries
49
+ this.passwordHistory = this.passwordHistory.slice(
50
+ 0,
51
+ this.settings.maxPasswordHistory
52
+ );
53
+ }
54
+
55
+ checkPasswordReuse(passwordHash) {
56
+ if (!this.settings.preventPasswordReuse) {
57
+ return false;
58
+ }
59
+
60
+ return this.passwordHistory.some(
61
+ (entry) => entry.passwordHash === passwordHash
62
+ );
63
+ }
64
+
65
+ updateLastPasswordChange() {
66
+ this.lastPasswordChange = new Date().toISOString();
67
+ }
68
+
69
+ toJSON() {
70
+ return {
71
+ version: this.version,
72
+ created: this.created,
73
+ lastPasswordChange: this.lastPasswordChange,
74
+ entries: this.entries, // This is now an object with IDs as keys
75
+ passwordHistory: this.passwordHistory,
76
+ settings: this.settings,
77
+ isDefault: this.isDefault,
78
+ };
79
+ }
80
+
81
+ static fromJSON(data, name) {
82
+ return new Vault({
83
+ name,
84
+ ...data,
85
+ });
86
+ }
87
+ }
@@ -0,0 +1,54 @@
1
+ import crypto from 'crypto';
2
+
3
+ export class CryptographyService {
4
+ static ITERATIONS = 100000;
5
+ static KEY_LENGTH = 32;
6
+ static IV_LENGTH = 16;
7
+ static ALGORITHM = 'aes-256-gcm';
8
+ static HASH_ALGORITHM = 'sha512';
9
+
10
+ static generateSalt(length = 32) {
11
+ return crypto.randomBytes(length);
12
+ }
13
+
14
+ static deriveKey(password, salt, iterations = this.ITERATIONS) {
15
+ return crypto.pbkdf2Sync(
16
+ password,
17
+ salt,
18
+ iterations,
19
+ this.KEY_LENGTH,
20
+ this.HASH_ALGORITHM
21
+ );
22
+ }
23
+
24
+ static encrypt(data, key) {
25
+ const iv = crypto.randomBytes(this.IV_LENGTH);
26
+ const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv);
27
+
28
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
29
+ encrypted += cipher.final('hex');
30
+
31
+ const authTag = cipher.getAuthTag();
32
+
33
+ return {
34
+ encrypted,
35
+ authTag: authTag.toString('hex'),
36
+ iv: iv.toString('hex'),
37
+ };
38
+ }
39
+
40
+ static decrypt(encryptedData, key) {
41
+ const iv = Buffer.from(encryptedData.iv, 'hex');
42
+ const decipher = crypto.createDecipheriv(this.ALGORITHM, key, iv);
43
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
44
+
45
+ let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
46
+ decrypted += decipher.final('utf8');
47
+
48
+ return JSON.parse(decrypted);
49
+ }
50
+
51
+ static hashPassword(password) {
52
+ return crypto.createHash('sha256').update(password).digest('hex');
53
+ }
54
+ }