-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.js
More file actions
307 lines (271 loc) · 10 KB
/
Copy pathauth.js
File metadata and controls
307 lines (271 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/**
* Koda - Intelligent Browser Automation Library
* This project uses Koda by Trent Pierce
* https://github.com/TrentPierce/Koda
* Licensed under the Koda Non-Commercial License
*
* Copyright (c) 2026 Trent Pierce. All rights reserved.
* See LICENSE file for full terms.
*/
// Handle optional dependency
let keytar;
try {
keytar = require('keytar');
} catch (error) {
console.warn('[Auth] keytar not installed. Secure credential storage will use fallback.');
console.warn('[Auth] Install with: npm install keytar');
keytar = null;
}
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const SERVICE_NAME = 'agentic-browser';
const ACCOUNT_NAME = 'database-encryption';
const SALT_FILE = path.join(__dirname, '.salt');
const FALLBACK_FILE = path.join(__dirname, '.auth_store');
// In-memory fallback storage when keytar is not available
let fallbackStorage = new Map();
// Load fallback storage from file
try {
if (fs.existsSync(FALLBACK_FILE)) {
const data = fs.readFileSync(FALLBACK_FILE, 'utf8');
fallbackStorage = new Map(JSON.parse(data));
console.log('[Auth] Loaded auth store from file');
}
} catch (e) {
console.error('[Auth] Failed to load auth store:', e.message);
}
function saveFallback() {
try {
fs.writeFileSync(FALLBACK_FILE, JSON.stringify(Array.from(fallbackStorage.entries())));
} catch (e) {
console.error('[Auth] Failed to save auth store:', e.message);
}
}
class AuthManager {
constructor() {
this.isAuthenticated = false;
this.derivedKey = null;
this.salt = null;
}
/**
* Check if secure credential storage is available
*/
static isSecureStorageAvailable() {
return keytar !== null;
}
/**
* Get or generate the salt for key derivation
* Each installation gets a unique random salt
*/
getSalt() {
if (this.salt) return this.salt;
try {
if (fs.existsSync(SALT_FILE)) {
this.salt = fs.readFileSync(SALT_FILE);
if (this.salt.length === 32) {
return this.salt;
}
}
} catch (error) {
console.error('[Auth] Error reading salt file:', error.message);
}
// Generate new random salt
this.salt = crypto.randomBytes(32);
try {
fs.writeFileSync(SALT_FILE, this.salt, { mode: 0o600 });
console.log('[Auth] Generated new unique salt');
} catch (error) {
console.error('[Auth] Error saving salt:', error.message);
}
return this.salt;
}
async isPasswordSet() {
try {
if (keytar) {
const passwordHash = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
return passwordHash !== null;
} else {
// Fallback: check in-memory storage
return fallbackStorage.has(`${SERVICE_NAME}:${ACCOUNT_NAME}`);
}
} catch (error) {
console.error('[Auth] Error checking password:', error.message);
return false;
}
}
async setPassword(password) {
// Validate password strength
if (!this.validatePassword(password)) {
throw new Error('Password must be at least 8 characters long');
}
try {
// Hash the password for storage (separate from encryption key)
const passwordHash = this.hashPassword(password);
// Store hash in system keychain or fallback storage
if (keytar) {
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, passwordHash);
} else {
fallbackStorage.set(`${SERVICE_NAME}:${ACCOUNT_NAME}`, passwordHash);
saveFallback();
console.warn('[Auth] Using fallback storage (keytar not available)');
}
this.derivedKey = this.deriveKey(password);
this.isAuthenticated = true;
console.log('[Auth] Password set successfully');
return { success: true, derivedKey: this.derivedKey };
} catch (error) {
console.error('[Auth] Error setting password:', error.message);
throw error;
}
}
/**
* Auto-login using stored credentials (no password prompt needed)
* The derived key is regenerated from saved state
*/
async autoLogin() {
try {
let storedHash;
if (keytar) {
storedHash = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
} else {
storedHash = fallbackStorage.get(`${SERVICE_NAME}:${ACCOUNT_NAME}`);
}
if (!storedHash) {
return { success: false, error: 'No credentials stored' };
}
// Use the stored hash itself as input to derive the encryption key
// This allows auto-login without storing the actual password
const salt = this.getSalt();
this.derivedKey = crypto.pbkdf2Sync(
storedHash, // Use the hash as the "password" for key derivation
Buffer.concat([salt, Buffer.from('auto-encryption')]),
100000,
32,
'sha256'
).toString('hex');
this.isAuthenticated = true;
console.log('[Auth] Auto-login successful');
return { success: true, derivedKey: this.derivedKey };
} catch (error) {
console.error('[Auth] Auto-login failed:', error.message);
return { success: false, error: error.message };
}
}
/**
* Hash password for secure storage verification
*/
hashPassword(password) {
const salt = this.getSalt();
return crypto.pbkdf2Sync(
password,
Buffer.concat([salt, Buffer.from('verification')]),
100000,
64,
'sha512'
).toString('hex');
}
async verifyPassword(password) {
try {
let storedHash;
if (keytar) {
storedHash = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
} else {
storedHash = fallbackStorage.get(`${SERVICE_NAME}:${ACCOUNT_NAME}`);
}
if (!storedHash) {
return { success: false, error: 'No password set' };
}
// Hash the provided password and compare
const inputHash = this.hashPassword(password);
// Check if stored value is a valid 128-char hex hash (64 bytes as hex)
const isHashFormat = storedHash.length === 128 && /^[a-f0-9]+$/i.test(storedHash);
if (isHashFormat) {
// New hash format - use timing-safe comparison
try {
const inputBuffer = Buffer.from(inputHash, 'hex');
const storedBuffer = Buffer.from(storedHash, 'hex');
if (inputBuffer.length === storedBuffer.length &&
crypto.timingSafeEqual(inputBuffer, storedBuffer)) {
this.derivedKey = this.deriveKey(password);
this.isAuthenticated = true;
console.log('[Auth] Password verified successfully');
return { success: true, derivedKey: this.derivedKey };
}
} catch (e) {
// Fall through to simple comparison
}
} else {
// Old plaintext format - check and migrate
if (password === storedHash) {
console.log('[Auth] Migrating from plaintext to hashed password');
// Upgrade to hashed format
if (keytar) {
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, inputHash);
} else {
fallbackStorage.set(`${SERVICE_NAME}:${ACCOUNT_NAME}`, inputHash);
saveFallback();
}
this.derivedKey = this.deriveKey(password);
this.isAuthenticated = true;
return { success: true, derivedKey: this.derivedKey };
}
}
return { success: false, error: 'Invalid password' };
} catch (error) {
console.error('[Auth] Error verifying password:', error.message);
return { success: false, error: error.message };
}
}
validatePassword(password) {
if (!password || typeof password !== 'string') {
return false;
}
return password.length >= 8;
}
deriveKey(password) {
const salt = this.getSalt();
// Use PBKDF2 to derive a strong encryption key
return crypto.pbkdf2Sync(
password,
Buffer.concat([salt, Buffer.from('encryption')]),
100000,
32,
'sha256'
).toString('hex');
}
getDerivedKey() {
if (!this.isAuthenticated || !this.derivedKey) {
throw new Error('Not authenticated');
}
return this.derivedKey;
}
async resetPassword() {
try {
if (keytar) {
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
} else {
fallbackStorage.delete(`${SERVICE_NAME}:${ACCOUNT_NAME}`);
saveFallback();
}
// Also remove salt to start fresh
if (fs.existsSync(SALT_FILE)) {
fs.unlinkSync(SALT_FILE);
}
this.isAuthenticated = false;
this.derivedKey = null;
this.salt = null;
console.log('[Auth] Password reset');
return { success: true };
} catch (error) {
console.error('[Auth] Error resetting password:', error.message);
throw error;
}
}
logout() {
this.isAuthenticated = false;
this.derivedKey = null;
console.log('[Auth] Logged out');
}
}
module.exports = AuthManager;