Skip to content

Commit c179ac8

Browse files
committed
security: implement secure error handling to prevent information disclosure
- Add SecureError system with structured error codes and severity levels - Update encryption service with secure error handling for crypto operations - Enhance WebSocket service with secure authentication and message errors - Improve API service error handling for network and auth failures - Update notes operations with comprehensive secure error management - Prevent sensitive data exposure in error messages and logs - Add context-aware error logging with development-only stack traces
1 parent 27a200c commit c179ac8

5 files changed

Lines changed: 527 additions & 90 deletions

File tree

src/hooks/useNotesOperations.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { api } from '@/lib/api/api';
33
import type { Note, Folder } from '@/types/note';
44
import type { UseWebSocketReturn } from './useWebSocket';
55
import { secureLogger } from '@/lib/utils/secureLogger';
6+
import { SecureError, logSecureError, sanitizeError } from '@/lib/errors/SecureError';
67

78
interface UseNotesOperationsParams {
89
folders: Folder[];
@@ -45,8 +46,11 @@ export function useNotesOperations({
4546
) => {
4647
try {
4748
if (!encryptionReady) {
48-
throw new Error(
49-
'Encryption not ready. Please wait a moment and try again.'
49+
throw new SecureError(
50+
'Note creation attempted without encryption ready',
51+
'Encryption not ready. Please wait a moment and try again.',
52+
'CRYPTO_001',
53+
'medium'
5054
);
5155
}
5256

@@ -76,9 +80,11 @@ export function useNotesOperations({
7680

7781
return noteWithFolder;
7882
} catch (error) {
83+
const secureError = sanitizeError(error, 'Failed to create note');
84+
logSecureError(secureError, 'useNotesOperations.createNote');
7985
secureLogger.error('Note creation failed', error);
80-
setError('Failed to create note');
81-
throw error;
86+
setError(secureError.userMessage);
87+
throw secureError;
8288
}
8389
},
8490
[
@@ -191,8 +197,10 @@ export function useNotesOperations({
191197
webSocket.sendNoteUpdate(noteId, updates);
192198
}
193199
} catch (error) {
200+
const secureError = sanitizeError(error, 'Failed to update note');
201+
logSecureError(secureError, 'useNotesOperations.updateNote.immediate');
194202
secureLogger.error('Failed to update note:', error);
195-
setError('Failed to update note');
203+
setError(secureError.userMessage);
196204
void loadData();
197205
}
198206
} else {
@@ -202,8 +210,11 @@ export function useNotesOperations({
202210
!encryptionReady &&
203211
(updates.title !== undefined || updates.content !== undefined)
204212
) {
205-
throw new Error(
206-
'Encryption not ready. Please wait a moment and try again.'
213+
throw new SecureError(
214+
'Note update attempted without encryption ready',
215+
'Encryption not ready. Please wait a moment and try again.',
216+
'CRYPTO_001',
217+
'medium'
207218
);
208219
}
209220

@@ -224,11 +235,13 @@ export function useNotesOperations({
224235

225236
saveTimeoutsRef.current.delete(noteId);
226237
} catch (error) {
238+
const secureError = sanitizeError(error, 'Failed to update note');
239+
logSecureError(secureError, 'useNotesOperations.updateNote.delayed');
227240
secureLogger.error('Failed to update note (delayed):', error);
228-
if (error instanceof Error && error.message.includes('encrypt')) {
241+
if (error instanceof SecureError && error.code === 'CRYPTO_001') {
229242
setError('Failed to encrypt note changes. Please try again.');
230243
} else {
231-
setError('Failed to update note');
244+
setError(secureError.userMessage);
232245
}
233246
void loadData();
234247
}
@@ -261,8 +274,10 @@ export function useNotesOperations({
261274
webSocket.sendNoteDeleted(noteId);
262275
}
263276
} catch (error) {
277+
const secureError = sanitizeError(error, 'Failed to delete note');
278+
logSecureError(secureError, 'useNotesOperations.deleteNote');
264279
secureLogger.error('Failed to delete note:', error);
265-
setError('Failed to delete note');
280+
setError(secureError.userMessage);
266281
}
267282
},
268283
[updateNote, webSocket, setError]
@@ -300,8 +315,10 @@ export function useNotesOperations({
300315
webSocket.sendNoteUpdate(noteId, { starred: updatedNote.starred });
301316
}
302317
} catch (error) {
318+
const secureError = sanitizeError(error, 'Failed to toggle star');
319+
logSecureError(secureError, 'useNotesOperations.toggleStar');
303320
secureLogger.error('Failed to toggle star:', error);
304-
setError('Failed to toggle star');
321+
setError(secureError.userMessage);
305322
}
306323
},
307324
[
@@ -319,6 +336,8 @@ export function useNotesOperations({
319336
try {
320337
await updateNote(noteId, { archived: true });
321338
} catch (error) {
339+
const secureError = sanitizeError(error, 'Failed to archive note');
340+
logSecureError(secureError, 'useNotesOperations.archiveNote');
322341
secureLogger.error('Failed to archive note:', error);
323342
}
324343
},
@@ -340,8 +359,10 @@ export function useNotesOperations({
340359
webSocket.sendNoteUpdate(noteId, { deleted: restoredNote.deleted });
341360
}
342361
} catch (error) {
362+
const secureError = sanitizeError(error, 'Failed to restore note');
363+
logSecureError(secureError, 'useNotesOperations.restoreNote');
343364
secureLogger.error('Failed to restore note:', error);
344-
setError('Failed to restore note');
365+
setError(secureError.userMessage);
345366
}
346367
},
347368
[convertApiNote, setNotes, webSocket, setError]
@@ -381,8 +402,10 @@ export function useNotesOperations({
381402
});
382403
}
383404
} catch (error) {
405+
const secureError = sanitizeError(error, 'Failed to hide note');
406+
logSecureError(secureError, 'useNotesOperations.hideNote');
384407
secureLogger.error('Failed to hide note:', error);
385-
setError('Failed to hide note');
408+
setError(secureError.userMessage);
386409
}
387410
},
388411
[
@@ -429,8 +452,10 @@ export function useNotesOperations({
429452
});
430453
}
431454
} catch (error) {
455+
const secureError = sanitizeError(error, 'Failed to unhide note');
456+
logSecureError(secureError, 'useNotesOperations.unhideNote');
432457
secureLogger.error('Failed to unhide note:', error);
433-
setError('Failed to unhide note');
458+
setError(secureError.userMessage);
434459
}
435460
},
436461
[
@@ -498,9 +523,11 @@ export function useNotesOperations({
498523

499524
return newFolder;
500525
} catch (error) {
526+
const secureError = sanitizeError(error, 'Failed to create folder');
527+
logSecureError(secureError, 'useNotesOperations.createFolder');
501528
secureLogger.error('Failed to create folder:', error);
502-
setError('Failed to create folder');
503-
throw error;
529+
setError(secureError.userMessage);
530+
throw secureError;
504531
}
505532
},
506533
[setFolders, webSocket, setError]
@@ -529,9 +556,11 @@ export function useNotesOperations({
529556

530557
return updatedFolder;
531558
} catch (error) {
559+
const secureError = sanitizeError(error, 'Failed to update folder');
560+
logSecureError(secureError, 'useNotesOperations.updateFolder');
532561
secureLogger.error('Failed to update folder:', error);
533-
setError('Failed to update folder');
534-
throw error;
562+
setError(secureError.userMessage);
563+
throw secureError;
535564
}
536565
},
537566
[safeConvertDates, setFolders, webSocket, setError]
@@ -560,8 +589,10 @@ export function useNotesOperations({
560589
webSocket.sendFolderDeleted(folderId);
561590
}
562591
} catch (error) {
592+
const secureError = sanitizeError(error, 'Failed to delete folder');
593+
logSecureError(secureError, 'useNotesOperations.deleteFolder');
563594
secureLogger.error('Failed to delete folder:', error);
564-
setError('Failed to delete folder');
595+
setError(secureError.userMessage);
565596
}
566597
},
567598
[

src/lib/api/api.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import {
33
decryptNoteData,
44
isNoteEncrypted,
55
} from '../encryption';
6+
import { SecureError, logSecureError, sanitizeError } from '../errors/SecureError';
67

78
const VITE_API_URL = import.meta.env.VITE_API_URL as string;
89

910
if (!VITE_API_URL) {
10-
throw new Error(
11-
'Missing API Base URL - Please add VITE_API_URL to your environment variables'
11+
throw new SecureError(
12+
'VITE_API_URL environment variable not set',
13+
'Configuration error. Please contact support.',
14+
'NETWORK_003',
15+
'critical'
1216
);
1317
}
1418

@@ -100,14 +104,22 @@ class ClerkEncryptedApiService {
100104
options: RequestInit = {}
101105
): Promise<T> {
102106
if (!this.getToken) {
103-
throw new Error(
104-
'Token provider not set. Make sure to call setTokenProvider first.'
107+
throw new SecureError(
108+
'API token provider not configured',
109+
'Authentication setup error. Please try logging in again.',
110+
'AUTH_001',
111+
'high'
105112
);
106113
}
107114

108115
const token = await this.getToken();
109116
if (!token) {
110-
throw new Error('No authentication token available');
117+
throw new SecureError(
118+
'No authentication token available from provider',
119+
'Please log in to continue.',
120+
'AUTH_002',
121+
'high'
122+
);
111123
}
112124

113125
const url = `${VITE_API_URL.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`;
@@ -125,25 +137,47 @@ class ClerkEncryptedApiService {
125137
const response = await fetch(url, config);
126138

127139
if (response.status === 401) {
128-
throw new Error('Authentication required');
140+
throw new SecureError(
141+
'API request returned 401 unauthorized',
142+
'Your session has expired. Please log in again.',
143+
'AUTH_002',
144+
'high'
145+
);
129146
}
130147

131148
if (!response.ok) {
132149
const errorText = await response.text();
133-
console.error(`API Error: ${response.status} - ${errorText}`);
134-
throw new Error(`API Error: ${response.status} - ${errorText}`);
150+
const secureError = new SecureError(
151+
`API Error: ${response.status} - ${errorText}`,
152+
response.status >= 500
153+
? 'Server error occurred. Please try again later.'
154+
: 'Request failed. Please try again.',
155+
response.status >= 500 ? 'NETWORK_003' : 'NETWORK_002',
156+
response.status >= 500 ? 'medium' : 'low'
157+
);
158+
logSecureError(secureError, 'ApiService.request');
159+
throw secureError;
135160
}
136161

137162
return response.json() as Promise<T>;
138163
} catch (error) {
139-
console.error('API Request failed:', error);
140-
throw error;
164+
if (error instanceof SecureError) {
165+
throw error;
166+
}
167+
const secureError = sanitizeError(error, 'Network request failed. Please check your connection.');
168+
logSecureError(secureError, 'ApiService.request');
169+
throw secureError;
141170
}
142171
}
143172

144173
private async decryptApiNote(apiNote: ApiNote): Promise<ApiNote> {
145174
if (!this.currentUserId) {
146-
throw new Error('User ID not set for encryption');
175+
throw new SecureError(
176+
'Decryption attempted without user ID set',
177+
'Authentication required for decryption',
178+
'CRYPTO_002',
179+
'high'
180+
);
147181
}
148182

149183
if (isNoteEncrypted(apiNote)) {
@@ -162,7 +196,8 @@ class ClerkEncryptedApiService {
162196
content,
163197
};
164198
} catch (error) {
165-
console.error('Failed to decrypt note:', apiNote.id, error);
199+
const secureError = sanitizeError(error, 'Failed to decrypt note');
200+
logSecureError(secureError, `ApiService.decryptApiNote:${apiNote.id}`);
166201
return {
167202
...apiNote,
168203
title: '🔒 Encrypted Note (Decryption Failed)',
@@ -185,7 +220,12 @@ class ClerkEncryptedApiService {
185220
salt: string;
186221
}> {
187222
if (!this.currentUserId) {
188-
throw new Error('User ID not set for encryption');
223+
throw new SecureError(
224+
'Encryption attempted without user ID set',
225+
'Authentication required for encryption',
226+
'CRYPTO_001',
227+
'high'
228+
);
189229
}
190230

191231
return encryptNoteData(this.currentUserId, title, content);

0 commit comments

Comments
 (0)