Skip to content

Commit d03bd90

Browse files
authored
Enhance storage with encryption and update auth flow
Refactor storage methods to support encryption for sensitive data and update user authentication flow.
1 parent e9d837c commit d03bd90

File tree

1 file changed

+108
-20
lines changed

1 file changed

+108
-20
lines changed

Build/src/main.js

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,62 @@ import { NotificationManager } from "./notifications.js";
1212
import { ContextMenuManager } from "./contextMenu.js";
1313
import { 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
1672
class 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
899987
document.addEventListener("DOMContentLoaded", () => {
900988
window.app = new HTMLChatApp();
901-
});
989+
});

0 commit comments

Comments
 (0)