@@ -12,6 +12,62 @@ import { NotificationManager } from "./notifications.js";
1212import { ContextMenuManager } from "./contextMenu.js" ;
1313import { ModeratorTools } from "./moderatorTools.js" ;
1414
15+ // WebCrypto-based encrypt/decrypt helpers for sensitive values
16+ async function getKeyFromPassphrase ( passphrase , salt ) {
17+ const encoder = new TextEncoder ( ) ;
18+ const keyMaterial = await window . crypto . subtle . importKey (
19+ "raw" ,
20+ encoder . encode ( passphrase ) ,
21+ "PBKDF2" ,
22+ false ,
23+ [ "deriveKey" ]
24+ ) ;
25+ return window . crypto . subtle . deriveKey (
26+ {
27+ name : "PBKDF2" ,
28+ salt : salt ,
29+ iterations : 50000 ,
30+ hash : "SHA-256"
31+ } ,
32+ keyMaterial ,
33+ { name : "AES-GCM" , length : 256 } ,
34+ false ,
35+ [ "encrypt" , "decrypt" ]
36+ ) ;
37+ }
38+
39+ async function encryptData ( plain , passphrase ) {
40+ const encoder = new TextEncoder ( ) ;
41+ const iv = window . crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
42+ const salt = window . crypto . getRandomValues ( new Uint8Array ( 16 ) ) ;
43+ const key = await getKeyFromPassphrase ( passphrase , salt ) ;
44+ const ciphertext = await window . crypto . subtle . encrypt (
45+ { name : "AES-GCM" , iv } ,
46+ key ,
47+ encoder . encode ( plain )
48+ ) ;
49+ // Return salt + iv + ciphertext as Base64
50+ const dataBuffer = new Uint8Array ( salt . length + iv . length + ciphertext . byteLength ) ;
51+ dataBuffer . set ( salt , 0 ) ;
52+ dataBuffer . set ( iv , salt . length ) ;
53+ dataBuffer . set ( new Uint8Array ( ciphertext ) , salt . length + iv . length ) ;
54+ return btoa ( String . fromCharCode . apply ( null , dataBuffer ) ) ;
55+ }
56+
57+ async function decryptData ( data_b64 , passphrase ) {
58+ const raw = Uint8Array . from ( atob ( data_b64 ) , c => c . charCodeAt ( 0 ) ) ;
59+ const salt = raw . slice ( 0 , 16 ) ;
60+ const iv = raw . slice ( 16 , 28 ) ;
61+ const ciphertext = raw . slice ( 28 ) ;
62+ const key = await getKeyFromPassphrase ( passphrase , salt ) ;
63+ const decrypted = await window . crypto . subtle . decrypt (
64+ { name : "AES-GCM" , iv } ,
65+ key ,
66+ ciphertext
67+ ) ;
68+ return new TextDecoder ( ) . decode ( decrypted ) ;
69+ }
70+
1571// Global app state
1672class HTMLChatApp {
1773 constructor ( ) {
@@ -159,18 +215,38 @@ class HTMLChatApp {
159215 }
160216
161217 // Simple storage helpers
162- saveToStorage ( key , data ) {
218+ async saveToStorage ( key , data , passphrase = null ) {
163219 try {
164- localStorage . setItem ( key , JSON . stringify ( data ) ) ;
220+ if ( key === "htmlchat_auth_token" ) {
221+ // Encrypt sensitive token with passphrase before storage
222+ if ( ! passphrase ) throw new Error ( "Missing passphrase for sensitive storage" ) ;
223+ const encrypted = await encryptData ( data , passphrase ) ;
224+ localStorage . setItem ( key , JSON . stringify ( { encrypted : encrypted } ) ) ;
225+ } else {
226+ localStorage . setItem ( key , JSON . stringify ( data ) ) ;
227+ }
165228 } catch ( e ) {
166229 console . warn ( "Storage failed:" , e ) ;
167230 }
168231 }
169232
170- loadFromStorage ( key ) {
233+ async loadFromStorage ( key , passphrase = null ) {
171234 try {
172235 const data = localStorage . getItem ( key ) ;
173- return data ? JSON . parse ( data ) : null ;
236+ if ( key === "htmlchat_auth_token" && data && passphrase ) {
237+ const obj = JSON . parse ( data ) ;
238+ if ( obj && obj . encrypted ) {
239+ try {
240+ return await decryptData ( obj . encrypted , passphrase ) ;
241+ } catch ( de ) {
242+ console . warn ( "Failed to decrypt auth token:" , de ) ;
243+ return null ;
244+ }
245+ }
246+ return null ;
247+ } else {
248+ return data ? JSON . parse ( data ) : null ;
249+ }
174250 } catch ( e ) {
175251 console . warn ( "Load failed:" , e ) ;
176252 return null ;
@@ -179,30 +255,42 @@ class HTMLChatApp {
179255
180256 async init ( ) {
181257 // Get or prompt for username
182- this . user = this . loadFromStorage ( "htmlchat_user" ) ;
183- this . authToken = this . loadFromStorage ( "htmlchat_auth_token" ) ;
258+ this . user = await this . loadFromStorage ( "htmlchat_user" ) ;
259+ this . authToken = null ;
260+ this . _authPassphrase = null ;
184261
185262 if ( ! this . user ) {
186263 do {
187264 this . user = prompt ( "Enter your nickname:" ) || "" ;
188265 this . user = this . user . trim ( ) . substring ( 0 , 20 ) ;
189266 } while ( ! this . user ) ;
190- this . saveToStorage ( "htmlchat_user" , this . user ) ;
267+ await this . saveToStorage ( "htmlchat_user" , this . user ) ;
191268
192- // If user is NellowTCS, prompt for moderator password
269+ // If user is NellowTCS, prompt for moderator password & passphrase to encrypt
193270 if ( this . user . toLowerCase ( ) === 'nellowtcs' ) {
194- const authPassword = prompt ( "Enter moderator password:" ) ;
195- if ( authPassword ) {
196- this . authToken = authPassword ;
197- this . saveToStorage ( "htmlchat_auth_token" , this . authToken ) ;
198- }
271+ let pw = prompt ( "Enter moderator password:" ) ;
272+ if ( pw ) {
273+ // Ask for a passphrase to encrypt the saved token (can use same value for simplicity)
274+ let passphrase = prompt ( "Provide a passphrase to protect your moderator token:" , "" ) ;
275+ if ( ! passphrase ) passphrase = pw ; // fallback for less friction
276+ this . _authPassphrase = passphrase ;
277+ this . authToken = pw ;
278+ await this . saveToStorage ( "htmlchat_auth_token" , this . authToken , this . _authPassphrase ) ;
199279 }
200- } else if ( this . user . toLowerCase ( ) === 'nellowtcs' && ! this . authToken ) {
201- // Existing NellowTCS user without saved auth token
202- const authPassword = prompt ( "Enter moderator password:" ) ;
203- if ( authPassword ) {
204- this . authToken = authPassword ;
205- this . saveToStorage ( "htmlchat_auth_token" , this . authToken ) ;
280+ } else if ( this . user . toLowerCase ( ) === 'nellowtcs' ) {
281+ // Try to load existing moderator token
282+ let passphrase = prompt ( "Enter passphrase to unlock moderator password:" ) ;
283+ this . _authPassphrase = passphrase ;
284+ if ( passphrase ) {
285+ this . authToken = await this . loadFromStorage ( "htmlchat_auth_token" , passphrase ) ;
286+ // Fallback: If not found or passphrase fails, allow prompt for token (+store new encrypted)
287+ if ( ! this . authToken ) {
288+ let pw = prompt ( "Enter moderator password:" ) ;
289+ if ( pw ) {
290+ this . authToken = pw ;
291+ await this . saveToStorage ( "htmlchat_auth_token" , pw , passphrase ) ;
292+ }
293+ }
206294 }
207295 }
208296
@@ -898,4 +986,4 @@ window.exportChat = function () {
898986// Initialize app when DOM is loaded
899987document . addEventListener ( "DOMContentLoaded" , ( ) => {
900988 window . app = new HTMLChatApp ( ) ;
901- } ) ;
989+ } ) ;
0 commit comments