1+ // Secure storage wrapper to encrypt sensitive data before storing in localStorage
2+ import { ENCRYPTION_CONFIG } from './constants' ;
3+
4+ class SecureStorage {
5+ private sessionKey : CryptoKey | null = null ;
6+ private isInitialized = false ;
7+
8+ async initialize ( ) : Promise < void > {
9+ if ( this . isInitialized ) return ;
10+
11+ // Generate a session-based encryption key
12+ // This key exists only in memory and is lost on page reload
13+ this . sessionKey = await crypto . subtle . generateKey (
14+ {
15+ name : ENCRYPTION_CONFIG . ALGORITHM ,
16+ length : ENCRYPTION_CONFIG . KEY_LENGTH ,
17+ } ,
18+ false , // Not extractable for security
19+ [ 'encrypt' , 'decrypt' ]
20+ ) ;
21+
22+ this . isInitialized = true ;
23+ }
24+
25+ async setSecureItem ( key : string , value : string ) : Promise < void > {
26+ if ( ! this . sessionKey ) {
27+ await this . initialize ( ) ;
28+ }
29+
30+ try {
31+ const encoder = new TextEncoder ( ) ;
32+ const data = encoder . encode ( value ) ;
33+
34+ // Generate random IV for each encryption
35+ const iv = crypto . getRandomValues ( new Uint8Array ( ENCRYPTION_CONFIG . IV_LENGTH ) ) ;
36+
37+ // Encrypt the data
38+ const encryptedData = await crypto . subtle . encrypt (
39+ { name : ENCRYPTION_CONFIG . ALGORITHM , iv } ,
40+ this . sessionKey ! ,
41+ data
42+ ) ;
43+
44+ // Store encrypted data + IV
45+ const payload = {
46+ encrypted : this . arrayBufferToBase64 ( encryptedData ) ,
47+ iv : this . arrayBufferToBase64 ( iv ) ,
48+ } ;
49+
50+ localStorage . setItem ( key , JSON . stringify ( payload ) ) ;
51+ } catch ( error ) {
52+ console . error ( 'Failed to encrypt storage item:' , error ) ;
53+ throw new Error ( 'Secure storage encryption failed' ) ;
54+ }
55+ }
56+
57+ async getSecureItem ( key : string ) : Promise < string | null > {
58+ if ( ! this . sessionKey ) {
59+ await this . initialize ( ) ;
60+ }
61+
62+ try {
63+ const stored = localStorage . getItem ( key ) ;
64+ if ( ! stored ) return null ;
65+
66+ const payload = JSON . parse ( stored ) ;
67+ if ( ! payload . encrypted || ! payload . iv ) {
68+ // Handle legacy plain-text storage by removing it
69+ localStorage . removeItem ( key ) ;
70+ return null ;
71+ }
72+
73+ // Decrypt the data
74+ const encryptedData = this . base64ToArrayBuffer ( payload . encrypted ) ;
75+ const iv = this . base64ToUint8Array ( payload . iv ) ;
76+
77+ const decryptedBuffer = await crypto . subtle . decrypt (
78+ { name : ENCRYPTION_CONFIG . ALGORITHM , iv } ,
79+ this . sessionKey ! ,
80+ encryptedData
81+ ) ;
82+
83+ const decoder = new TextDecoder ( ) ;
84+ return decoder . decode ( decryptedBuffer ) ;
85+ } catch ( error ) {
86+ console . error ( 'Failed to decrypt storage item:' , error ) ;
87+ // If decryption fails, remove the corrupted item
88+ localStorage . removeItem ( key ) ;
89+ return null ;
90+ }
91+ }
92+
93+ removeSecureItem ( key : string ) : void {
94+ localStorage . removeItem ( key ) ;
95+ }
96+
97+ // Clear all session keys on logout/cleanup
98+ clearSession ( ) : void {
99+ this . sessionKey = null ;
100+ this . isInitialized = false ;
101+ }
102+
103+ private arrayBufferToBase64 ( buffer : ArrayBuffer ) : string {
104+ const bytes = new Uint8Array ( buffer ) ;
105+ const chunks : string [ ] = [ ] ;
106+ const CHUNK_SIZE = 8192 ;
107+
108+ for ( let i = 0 ; i < bytes . length ; i += CHUNK_SIZE ) {
109+ const chunk = bytes . subarray ( i , Math . min ( i + CHUNK_SIZE , bytes . length ) ) ;
110+ chunks . push ( String . fromCharCode . apply ( null , Array . from ( chunk ) ) ) ;
111+ }
112+
113+ return btoa ( chunks . join ( '' ) ) ;
114+ }
115+
116+ private base64ToArrayBuffer ( base64 : string ) : ArrayBuffer {
117+ const bytes = this . base64ToUint8Array ( base64 ) ;
118+ return bytes . buffer . slice ( bytes . byteOffset , bytes . byteOffset + bytes . byteLength ) ;
119+ }
120+
121+ private base64ToUint8Array ( base64 : string ) : Uint8Array {
122+ const binary = atob ( base64 ) ;
123+ const bytes = new Uint8Array ( binary . length ) ;
124+ for ( let i = 0 ; i < binary . length ; i ++ ) {
125+ bytes [ i ] = binary . charCodeAt ( i ) ;
126+ }
127+ return bytes ;
128+ }
129+ }
130+
131+ export const secureStorage = new SecureStorage ( ) ;
0 commit comments